Add direct message composer to profile modal

This commit is contained in:
thePR0M3TH3AN committed 2026-01-18 19:48:43 -05:00
1 parent 576a73ae87
commit 1e547bc49c
2 files changed
+173

No files matched your search

+27
View File
@@ -1067,6 +1067,33 @@
aria-live="polite"
aria-label="Direct message threads"
></ul>
<div class="flex flex-col gap-3 border-t border-border/60 pt-4">
<label class="sr-only" for="profileMessageInput">Message</label>
<textarea
id="profileMessageInput"
class="input min-h-[96px]"
rows="3"
placeholder="Write a message…"
></textarea>
<div class="flex flex-wrap items-center justify-between gap-2">
<p
id="profileMessagesComposerHelper"
class="text-xs text-muted hidden"
role="status"
aria-live="polite"
>
Sign in to send messages.
</p>
<button
id="profileMessageSendBtn"
type="button"
class="btn focus-ring inline-flex items-center gap-2"
data-size="sm"
>
Send
</button>
</div>
</div>
</section>
<section
id="profilePaneHistory"
+146
View File
@@ -1100,6 +1100,9 @@ export class ProfileModalController {
this.profileMessagesStatus = null;
this.profileMessagesReloadButton = null;
this.profileMessagesPane = null;
this.profileMessageInput = null;
this.profileMessageSendButton = null;
this.profileMessagesComposerHelper = null;
this.walletUriInput = null;
this.walletDefaultZapInput = null;
this.walletSaveButton = null;
@@ -1355,6 +1358,12 @@ export class ProfileModalController {
document.getElementById("profileMessagesStatus") || null;
this.profileMessagesReloadButton =
document.getElementById("profileMessagesReload") || null;
this.profileMessageInput =
document.getElementById("profileMessageInput") || null;
this.profileMessageSendButton =
document.getElementById("profileMessageSendBtn") || null;
this.profileMessagesComposerHelper =
document.getElementById("profileMessagesComposerHelper") || null;
if (this.pendingMessagesRender) {
const { messages, actorPubkey } = this.pendingMessagesRender;
@@ -1945,6 +1954,7 @@ export class ProfileModalController {
}
this.updateMessagesReloadState();
this.updateMessageComposerState();
}
updateMessagesReloadState() {
@@ -1970,6 +1980,40 @@ export class ProfileModalController {
}
}
updateMessageComposerState() {
const input = this.profileMessageInput;
const button = this.profileMessageSendButton;
const helper = this.profileMessagesComposerHelper;
const shouldDisable = this.messagesLoadingState === "unauthenticated";
const applyDisabledState = (element) => {
if (!(element instanceof HTMLElement) || !("disabled" in element)) {
return;
}
element.disabled = shouldDisable;
if (shouldDisable) {
element.setAttribute("aria-disabled", "true");
} else {
element.removeAttribute("aria-disabled");
}
};
applyDisabledState(input);
applyDisabledState(button);
if (helper instanceof HTMLElement) {
if (shouldDisable) {
helper.textContent = "Sign in to send messages.";
helper.classList.remove("hidden");
helper.removeAttribute("hidden");
} else {
helper.classList.add("hidden");
helper.setAttribute("hidden", "");
}
}
}
setMessagesAnnouncement(message) {
if (!(this.profileMessagesStatus instanceof HTMLElement)) {
return;
@@ -2000,6 +2044,93 @@ export class ProfileModalController {
}
}
describeDirectMessageSendError(code) {
switch (code) {
case "sign-event-unavailable":
return "Connect a Nostr signer to send messages.";
case "encryption-unsupported":
return "Your signer does not support NIP-44 or NIP-04 encryption.";
case "extension-permission-denied":
return "Please grant your Nostr extension permission to send messages.";
case "missing-actor-pubkey":
return "We couldn’t determine your public key to send this message.";
case "nostr-uninitialized":
return "Direct messages are still connecting to relays. Please try again.";
case "signature-failed":
return "We couldn’t sign the message. Please reconnect your signer and try again.";
case "encryption-failed":
return "We couldn’t encrypt the message. Please try again.";
case "publish-failed":
return "Failed to deliver this message to any relay. Please try again.";
case "invalid-target":
return "Select a valid recipient before sending.";
case "empty-message":
return "Please enter a message.";
default:
return "Unable to send message. Please try again.";
}
}
async handleSendProfileMessage() {
const input = this.profileMessageInput;
if (!(input instanceof HTMLTextAreaElement)) {
return;
}
const message = typeof input.value === "string" ? input.value.trim() : "";
if (!message) {
this.showError("Please enter a message.");
return;
}
const targetCandidate =
this.resolveActiveDmActor() || this.directMessagesLastActor;
const target =
typeof targetCandidate === "string" ? targetCandidate.trim() : "";
if (!target) {
this.showError("Please select a message recipient.");
return;
}
if (
!this.services.nostrClient ||
typeof this.services.nostrClient.sendDirectMessage !== "function"
) {
this.showError("Direct message service unavailable.");
return;
}
const sendButton = this.profileMessageSendButton;
if (sendButton instanceof HTMLElement && "disabled" in sendButton) {
sendButton.disabled = true;
sendButton.setAttribute("aria-disabled", "true");
}
try {
const result = await this.services.nostrClient.sendDirectMessage(
target,
message,
);
if (result?.ok) {
input.value = "";
this.showSuccess("Message sent.");
void this.populateProfileMessages({ force: true, reason: "send-message" });
return;
}
const errorCode =
typeof result?.error === "string" ? result.error : "unknown";
userLogger.warn("[profileModal] Failed to send direct message:", errorCode);
this.showError(this.describeDirectMessageSendError(errorCode));
} catch (error) {
userLogger.error("[profileModal] Unexpected DM send failure:", error);
this.showError("Unable to send message. Please try again.");
} finally {
this.updateMessageComposerState();
}
}
clearProfileMessages({ message } = {}) {
this.directMessagesCache = [];
this.directMessagesLastActor = this.resolveActiveDmActor();
@@ -2673,6 +2804,21 @@ export class ProfileModalController {
});
}
if (this.profileMessageSendButton instanceof HTMLElement) {
this.profileMessageSendButton.addEventListener("click", () => {
void this.handleSendProfileMessage();
});
}
if (this.profileMessageInput instanceof HTMLElement) {
this.profileMessageInput.addEventListener("keydown", (event) => {
if (event.key === "Enter" && !event.shiftKey) {
event.preventDefault();
void this.handleSendProfileMessage();
}
});
}
if (this.walletUriInput instanceof HTMLElement) {
this.walletUriInput.addEventListener("focus", () => {
this.revealSecretInputValue(this.walletUriInput);