diff --git a/components/profile-modal.html b/components/profile-modal.html
index cbe1f87a..317d2ebe 100644
--- a/components/profile-modal.html
+++ b/components/profile-modal.html
@@ -1355,50 +1355,6 @@
role="tabpanel"
aria-labelledby="profileNavMessages"
>
-
-
-
DM relay hints
-
- Publish relay hints for direct messages as kind:10050 events. Use WSS-only relay URLs so encrypted DM delivery stays secure.
-
-
-
-
-
-
-
-
-
-
-
-
{
if (!popover.contains(event.target) && event.target !== settingsBtn && !settingsBtn.contains(event.target)) {
diff --git a/js/ui/dm/DMRelaySettings.js b/js/ui/dm/DMRelaySettings.js
new file mode 100644
index 00000000..6f2331e2
--- /dev/null
+++ b/js/ui/dm/DMRelaySettings.js
@@ -0,0 +1,80 @@
+export function DMRelaySettings({ document: doc } = {}) {
+ if (!doc) {
+ throw new Error("DMRelaySettings requires a document reference.");
+ }
+
+ const root = doc.createElement("div");
+ root.id = "profileMessagesRelayPanel";
+ root.className = "dm-relay-settings card space-y-4 p-5";
+
+ const heading = doc.createElement("div");
+ heading.className = "space-y-2";
+
+ const title = doc.createElement("h3");
+ title.className = "text-base font-semibold text-text";
+ title.textContent = "DM relay hints";
+ heading.appendChild(title);
+
+ const description = doc.createElement("p");
+ description.className = "text-sm text-muted";
+ description.innerHTML =
+ 'Publish relay hints for direct messages as kind:10050 events. Use WSS-only relay URLs so encrypted DM delivery stays secure.';
+ heading.appendChild(description);
+ root.appendChild(heading);
+
+ const addRow = doc.createElement("div");
+ addRow.className = "flex flex-col gap-3 sm:flex-row sm:items-end";
+
+ const label = doc.createElement("label");
+ label.className = "flex-1 text-sm text-muted-strong";
+ label.setAttribute("for", "profileMessagesRelayInput");
+
+ const labelTitle = doc.createElement("span");
+ labelTitle.className = "mb-2 block font-medium";
+ labelTitle.textContent = "Add a DM relay";
+ label.appendChild(labelTitle);
+
+ const input = doc.createElement("input");
+ input.id = "profileMessagesRelayInput";
+ input.type = "text";
+ input.inputMode = "url";
+ input.autocomplete = "off";
+ input.className = "input";
+ input.placeholder = "wss://relay.example.com";
+ label.appendChild(input);
+ addRow.appendChild(label);
+
+ const addButton = doc.createElement("button");
+ addButton.id = "profileMessagesRelayAdd";
+ addButton.type = "button";
+ addButton.className = "btn focus-ring";
+ addButton.textContent = "Add relay";
+ addRow.appendChild(addButton);
+ root.appendChild(addRow);
+
+ const publishRow = doc.createElement("div");
+ publishRow.className = "flex flex-wrap items-center gap-2";
+
+ const publishButton = doc.createElement("button");
+ publishButton.id = "profileMessagesRelayPublish";
+ publishButton.type = "button";
+ publishButton.className = "btn-ghost focus-ring text-xs";
+ publishButton.textContent = "Publish relay hints";
+ publishRow.appendChild(publishButton);
+
+ const status = doc.createElement("span");
+ status.id = "profileMessagesRelayStatus";
+ status.className = "text-xs text-muted";
+ status.setAttribute("role", "status");
+ status.setAttribute("aria-live", "polite");
+ publishRow.appendChild(status);
+ root.appendChild(publishRow);
+
+ const list = doc.createElement("ul");
+ list.id = "profileMessagesRelayList";
+ list.className = "grid-stack";
+ list.dataset.orientation = "vertical";
+ root.appendChild(list);
+
+ return root;
+}
diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js
index c6fef21c..8f25b020 100644
--- a/js/ui/profileModalController.js
+++ b/js/ui/profileModalController.js
@@ -1646,16 +1646,7 @@ export class ProfileModalController {
document.getElementById("profileMessagesPrivacyToggle") || null;
this.profileMessagesPrivacyMode =
document.getElementById("profileMessagesPrivacyMode") || null;
- this.profileMessagesRelayList =
- document.getElementById("profileMessagesRelayList") || null;
- this.profileMessagesRelayInput =
- document.getElementById("profileMessagesRelayInput") || null;
- this.profileMessagesRelayAddButton =
- document.getElementById("profileMessagesRelayAdd") || null;
- this.profileMessagesRelayPublishButton =
- document.getElementById("profileMessagesRelayPublish") || null;
- this.profileMessagesRelayStatus =
- document.getElementById("profileMessagesRelayStatus") || null;
+ this.cacheDmRelayElements();
this.profileMessagesUnreadDot =
document.getElementById("profileMessagesUnreadDot") || null;
this.dmAppShellContainer =
@@ -2251,6 +2242,63 @@ export class ProfileModalController {
setLinkPreviewAutoFetch(Boolean(enabled));
}
+ cacheDmRelayElements() {
+ this.profileMessagesRelayList =
+ document.getElementById("profileMessagesRelayList") || null;
+ this.profileMessagesRelayInput =
+ document.getElementById("profileMessagesRelayInput") || null;
+ this.profileMessagesRelayAddButton =
+ document.getElementById("profileMessagesRelayAdd") || null;
+ this.profileMessagesRelayPublishButton =
+ document.getElementById("profileMessagesRelayPublish") || null;
+ this.profileMessagesRelayStatus =
+ document.getElementById("profileMessagesRelayStatus") || null;
+ }
+
+ bindDmRelayControls() {
+ const bindOnce = (element, eventName, handler, key) => {
+ if (!(element instanceof HTMLElement)) {
+ return;
+ }
+ const datasetKey = key || "dmRelayBound";
+ if (element.dataset[datasetKey] === "true") {
+ return;
+ }
+ element.dataset[datasetKey] = "true";
+ element.addEventListener(eventName, handler);
+ };
+
+ bindOnce(
+ this.profileMessagesRelayAddButton,
+ "click",
+ () => {
+ void this.handleAddDmRelayPreference();
+ },
+ "dmRelayAddBound",
+ );
+
+ bindOnce(
+ this.profileMessagesRelayInput,
+ "keydown",
+ (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ void this.handleAddDmRelayPreference();
+ }
+ },
+ "dmRelayInputBound",
+ );
+
+ bindOnce(
+ this.profileMessagesRelayPublishButton,
+ "click",
+ () => {
+ void this.handlePublishDmRelayPreferences();
+ },
+ "dmRelayPublishBound",
+ );
+ }
+
async refreshDmRelayPreferences({ force = false } = {}) {
const owner = this.resolveActiveDmRelayOwner();
if (!owner) {
@@ -4545,6 +4593,11 @@ export class ProfileModalController {
onToggleTypingIndicators: (enabled) => {
this.handleTypingIndicatorsToggle(enabled);
},
+ onOpenSettings: () => {
+ this.cacheDmRelayElements();
+ this.bindDmRelayControls();
+ this.populateDmRelayPreferences();
+ },
});
} catch (error) {
this.dmAppShell = null;
@@ -5428,26 +5481,7 @@ export class ProfileModalController {
});
}
- if (this.profileMessagesRelayAddButton instanceof HTMLElement) {
- this.profileMessagesRelayAddButton.addEventListener("click", () => {
- void this.handleAddDmRelayPreference();
- });
- }
-
- if (this.profileMessagesRelayInput instanceof HTMLElement) {
- this.profileMessagesRelayInput.addEventListener("keydown", (event) => {
- if (event.key === "Enter") {
- event.preventDefault();
- void this.handleAddDmRelayPreference();
- }
- });
- }
-
- if (this.profileMessagesRelayPublishButton instanceof HTMLElement) {
- this.profileMessagesRelayPublishButton.addEventListener("click", () => {
- void this.handlePublishDmRelayPreferences();
- });
- }
+ this.bindDmRelayControls();
if (this.profileMessageSendButton instanceof HTMLElement) {
this.profileMessageSendButton.addEventListener("click", () => {