mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Add DM privacy indicators and relay hint publishing
This commit is contained in:
1 parent
e1d58a2530
commit
924ed9b482
9 files changed
+597
-8
No files matched your search
@@ -1055,6 +1055,7 @@
|
||||
<label
|
||||
for="profileMessagesPrivacyToggle"
|
||||
class="inline-flex items-center gap-2 text-xs font-medium text-muted-strong"
|
||||
title="NIP-17 gift-wraps your DM so relays only see the wrapper and relay hints, not the plaintext. It requires the recipient to publish kind 10050 relay hints."
|
||||
>
|
||||
<input
|
||||
id="profileMessagesPrivacyToggle"
|
||||
@@ -1065,6 +1066,59 @@
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card space-y-4 p-4">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold text-text">Your DM relay hints</h3>
|
||||
<p class="text-xs text-muted">
|
||||
Publish kind 10050 so contacts can deliver NIP-17 gift-wrapped DMs to you.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
id="profileMessagesRelayPublish"
|
||||
type="button"
|
||||
class="btn focus-ring inline-flex items-center gap-2"
|
||||
data-size="sm"
|
||||
title="Publishing kind 10050 makes your DM relay list public so others can deliver NIP-17 messages."
|
||||
>
|
||||
Publish relays
|
||||
</button>
|
||||
</div>
|
||||
<label class="flex-1 text-sm text-muted-strong" for="profileMessagesRelayInput">
|
||||
<span class="mb-2 block font-medium">Add a DM relay</span>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<input
|
||||
id="profileMessagesRelayInput"
|
||||
type="url"
|
||||
inputmode="url"
|
||||
autocomplete="off"
|
||||
class="input flex-1"
|
||||
placeholder="wss://relay.example.com"
|
||||
/>
|
||||
<button
|
||||
id="profileMessagesRelayAdd"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring inline-flex items-center gap-2"
|
||||
data-size="sm"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
<ul
|
||||
id="profileMessagesRelayList"
|
||||
class="grid-stack"
|
||||
data-orientation="vertical"
|
||||
aria-live="polite"
|
||||
aria-label="Your DM relay hints"
|
||||
></ul>
|
||||
<p
|
||||
id="profileMessagesRelayStatus"
|
||||
class="text-xs text-muted"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></p>
|
||||
</div>
|
||||
<div
|
||||
id="profileMessagesStatus"
|
||||
class="sr-only"
|
||||
@@ -1137,6 +1191,13 @@
|
||||
>
|
||||
Sign in to send messages.
|
||||
</p>
|
||||
<p
|
||||
id="profileMessagesPrivacyMode"
|
||||
class="text-xs text-muted"
|
||||
title="NIP-04 sends a direct encrypted DM; NIP-17 gift-wraps messages to reduce metadata exposure but relies on relay hints."
|
||||
>
|
||||
Privacy: NIP-04
|
||||
</p>
|
||||
<button
|
||||
id="profileMessageSendBtn"
|
||||
type="button"
|
||||
|
||||
@@ -151,6 +151,7 @@ import { updateWatchHistoryListWithDefaultClient } from "./nostrWatchHistoryFaca
|
||||
| NIP-71 video (`NOTE_TYPES.NIP71_VIDEO`) | `21` | `['title', <title>]`, optional `['published_at', <unix seconds>]`, optional `['alt', <text>]`, repeated `['imeta', ...]` entries describing NIP-92 media variants, optional `['duration', <seconds>]`, repeated `['text-track', <url>, <kind>, <language>]`, optional `['content-warning', <reason>]`, repeated `['segment', <start>, <end>, <title>, <thumbnail>]`, repeated hashtags `['t', <tag>]`, repeated participants `['p', <pubkey>, <relay?>]`, repeated references `['r', <url>]` | Plain text summary carried in the content field. Publishing is gated by the `FEATURE_PUBLISH_NIP71` runtime flag while the rollout stabilizes. |
|
||||
| NIP-71 short video (`NOTE_TYPES.NIP71_SHORT_VIDEO`) | `22` | Same as `NOTE_TYPES.NIP71_VIDEO`; the kind differentiates short-form presentations. | Plain text summary; gated by `FEATURE_PUBLISH_NIP71`. |
|
||||
| Relay list (`NOTE_TYPES.RELAY_LIST`) | `10002` | Repeating `['r', <relay url>]` tags, optionally with a marker of `'read'` or `'write'` to scope the relay; marker omitted for read/write relays | Empty content |
|
||||
| DM relay hints (`NOTE_TYPES.DM_RELAY_LIST`) | `10050` | Repeating `['relay', <relay url>]` tags to advertise delivery relays for NIP-17 gift-wrapped DMs | Empty content |
|
||||
| View counter (`NOTE_TYPES.VIEW_EVENT`) | `WATCH_HISTORY_KIND` (default `30079`) | Canonical tag set: `['t','view']`, a pointer tag (`['e', <eventId>]` or `['a', <address>]`), and a stable dedupe tag `['d', <view identifier>]`, with optional `['session','true']` when a session actor signs; schema overrides may append extra tags. `['video', ...]` is supported for legacy overrides only. | Optional plaintext message |
|
||||
| Watch history month (`NOTE_TYPES.WATCH_HISTORY_INDEX`) | `WATCH_HISTORY_KIND` (default `30079`) | Replaceable list tag `['d', `${WATCH_HISTORY_LIST_IDENTIFIER}:<YYYY-MM>`]` with optional `['month', <YYYY-MM>]` marker plus schema append tags; no chunk pointers required. | JSON payload `{ version, month: 'YYYY-MM', items: [{ id: <eventId\|address>, watched_at?: <unix seconds> }] }` |
|
||||
| Subscription list (`NOTE_TYPES.SUBSCRIPTION_LIST`) | `30000` | `['d', 'subscriptions']` | NIP-04/NIP-44 encrypted JSON array of NIP-51 follow-set tuples (e.g., `[['p', <hex>], …]`) |
|
||||
|
||||
@@ -69,6 +69,11 @@ import createPopover from "./ui/overlay/popoverEngine.js";
|
||||
import { createVideoSettingsMenuPanel } from "./ui/components/videoMenuRenderers.js";
|
||||
import moderationService from "./services/moderationService.js";
|
||||
import { sanitizeRelayList } from "./nostr/nip46Client.js";
|
||||
import { buildDmRelayListEvent } from "./nostrEventSchemas.js";
|
||||
import {
|
||||
publishEventToRelays,
|
||||
assertAnyRelayAccepted,
|
||||
} from "./nostrPublish.js";
|
||||
import {
|
||||
initViewCounter,
|
||||
subscribeToVideoViewCount,
|
||||
@@ -3093,6 +3098,132 @@ class Application {
|
||||
return stored.slice();
|
||||
}
|
||||
|
||||
async publishDmRelayPreferences({ pubkey, relays } = {}) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey || this.pubkey);
|
||||
if (!normalized) {
|
||||
const error = new Error("A valid pubkey is required to publish DM relays.");
|
||||
error.code = "invalid-pubkey";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const relayHints = sanitizeRelayList(Array.isArray(relays) ? relays : []);
|
||||
if (!relayHints.length) {
|
||||
const error = new Error("Add at least one DM relay before publishing.");
|
||||
error.code = "empty";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!nostrClient?.pool) {
|
||||
const error = new Error(
|
||||
"Nostr is not connected yet. Please try again once relays are ready.",
|
||||
);
|
||||
error.code = "nostr-uninitialized";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const signer = await nostrClient.ensureActiveSignerForPubkey(normalized);
|
||||
if (!signer || typeof signer.signEvent !== "function") {
|
||||
const error = new Error(
|
||||
"An active signer with signEvent support is required to publish DM relays.",
|
||||
);
|
||||
error.code = "nostr-extension-missing";
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (signer.type === "extension" && nostrClient.ensureExtensionPermissions) {
|
||||
const permissionResult = await nostrClient.ensureExtensionPermissions();
|
||||
if (!permissionResult?.ok) {
|
||||
userLogger.warn(
|
||||
"[Application] Signer permissions denied while publishing DM relays.",
|
||||
permissionResult?.error,
|
||||
);
|
||||
const error = new Error(
|
||||
"The active signer must allow signing before publishing DM relays.",
|
||||
);
|
||||
error.code = "extension-permission-denied";
|
||||
error.cause = permissionResult?.error;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const event = buildDmRelayListEvent({
|
||||
pubkey: normalized,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
relays: relayHints,
|
||||
});
|
||||
|
||||
const signedEvent = await signer.signEvent(event);
|
||||
|
||||
const relayTargets = sanitizeRelayList(
|
||||
Array.isArray(nostrClient.writeRelays) && nostrClient.writeRelays.length
|
||||
? nostrClient.writeRelays
|
||||
: Array.isArray(nostrClient.relays)
|
||||
? nostrClient.relays
|
||||
: [],
|
||||
);
|
||||
|
||||
if (!relayTargets.length) {
|
||||
const error = new Error("No relay targets are available for publishing.");
|
||||
error.code = "no-targets";
|
||||
throw error;
|
||||
}
|
||||
|
||||
const publishResults = await publishEventToRelays(
|
||||
nostrClient.pool,
|
||||
relayTargets,
|
||||
signedEvent,
|
||||
);
|
||||
|
||||
let publishSummary;
|
||||
try {
|
||||
publishSummary = assertAnyRelayAccepted(publishResults, {
|
||||
context: "dm relay hints update",
|
||||
message: "No relays accepted the DM relay list.",
|
||||
});
|
||||
} catch (publishError) {
|
||||
if (publishError?.relayFailures?.length) {
|
||||
publishError.relayFailures.forEach(
|
||||
({ url, error: relayError, reason }) => {
|
||||
userLogger.error(
|
||||
`[Application] Relay ${url} rejected DM relay list: ${reason}`,
|
||||
relayError || reason,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
throw publishError;
|
||||
}
|
||||
|
||||
if (publishSummary.failed.length) {
|
||||
publishSummary.failed.forEach(({ url, error: relayError }) => {
|
||||
const reason =
|
||||
relayError instanceof Error
|
||||
? relayError.message
|
||||
: relayError
|
||||
? String(relayError)
|
||||
: "publish failed";
|
||||
userLogger.warn(
|
||||
`[Application] Relay ${url} did not acknowledge DM relay hints: ${reason}`,
|
||||
relayError,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
event: signedEvent,
|
||||
accepted: publishSummary.accepted.map(({ url }) => url),
|
||||
failed: publishSummary.failed.map(({ url, error: relayError }) => ({
|
||||
url,
|
||||
error: relayError || null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
handleProfilePublishDmRelayPreferences({ relays, pubkey } = {}) {
|
||||
return this.publishDmRelayPreferences({ relays, pubkey });
|
||||
}
|
||||
|
||||
async fetchDmRelayHints(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
if (!normalized) {
|
||||
|
||||
@@ -24,6 +24,7 @@ export const NOTE_TYPES = Object.freeze({
|
||||
SUBSCRIPTION_LIST: "subscriptionList",
|
||||
USER_BLOCK_LIST: "userBlockList",
|
||||
HASHTAG_PREFERENCES: "hashtagPreferences",
|
||||
DM_RELAY_LIST: "dmRelayList",
|
||||
ADMIN_MODERATION_LIST: "adminModerationList",
|
||||
ADMIN_BLACKLIST: "adminBlacklist",
|
||||
ADMIN_WHITELIST: "adminWhitelist",
|
||||
@@ -309,6 +310,14 @@ const BASE_SCHEMAS = {
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: { format: "empty", description: "Content field unused." },
|
||||
},
|
||||
[NOTE_TYPES.DM_RELAY_LIST]: {
|
||||
type: NOTE_TYPES.DM_RELAY_LIST,
|
||||
label: "DM relay hints",
|
||||
kind: 10050,
|
||||
relayTagName: "relay",
|
||||
appendTags: DEFAULT_APPEND_TAGS,
|
||||
content: { format: "empty", description: "Content field unused." },
|
||||
},
|
||||
[NOTE_TYPES.VIEW_EVENT]: {
|
||||
type: NOTE_TYPES.VIEW_EVENT,
|
||||
label: "View counter",
|
||||
@@ -941,6 +950,42 @@ export function buildRelayListEvent(params) {
|
||||
};
|
||||
}
|
||||
|
||||
export function buildDmRelayListEvent(params) {
|
||||
const {
|
||||
pubkey,
|
||||
created_at,
|
||||
relays = [],
|
||||
additionalTags = [],
|
||||
} = params || {};
|
||||
const schema = getNostrEventSchema(NOTE_TYPES.DM_RELAY_LIST);
|
||||
const tags = [];
|
||||
const relayTagName = schema?.relayTagName || "relay";
|
||||
|
||||
if (Array.isArray(relays)) {
|
||||
relays.forEach((relay) => {
|
||||
const normalized = typeof relay === "string" ? relay.trim() : "";
|
||||
if (normalized) {
|
||||
tags.push([relayTagName, normalized]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
appendSchemaTags(tags, schema);
|
||||
|
||||
const sanitizedAdditionalTags = sanitizeAdditionalTags(additionalTags);
|
||||
if (sanitizedAdditionalTags.length) {
|
||||
tags.push(...sanitizedAdditionalTags.map((tag) => tag.slice()));
|
||||
}
|
||||
|
||||
return {
|
||||
kind: schema?.kind ?? 10050,
|
||||
pubkey,
|
||||
created_at,
|
||||
tags,
|
||||
content: "",
|
||||
};
|
||||
}
|
||||
|
||||
export function buildProfileMetadataEvent(params) {
|
||||
const {
|
||||
pubkey,
|
||||
|
||||
@@ -504,6 +504,9 @@ export default class ApplicationBootstrap {
|
||||
setDmRecipient: (pubkey) => app.setDmRecipientPubkey(pubkey),
|
||||
getDmRelayHints: (pubkey) => app.getDmRelayHints(pubkey),
|
||||
setDmRelayHints: (pubkey, hints) => app.setDmRelayHints(pubkey, hints),
|
||||
getDmRelayPreferences: (pubkey) => app.getDmRelayHints(pubkey),
|
||||
setDmRelayPreferences: (pubkey, hints) =>
|
||||
app.setDmRelayHints(pubkey, hints),
|
||||
};
|
||||
|
||||
const profileModalCallbacks = {
|
||||
@@ -541,6 +544,8 @@ export default class ApplicationBootstrap {
|
||||
onOpenRelays: (payload) => app.handleProfileUseDmRelays(payload),
|
||||
onTogglePrivacy: (payload) =>
|
||||
app.handleProfilePrivacyToggle(payload),
|
||||
onPublishDmRelayPreferences: (payload) =>
|
||||
app.handleProfilePublishDmRelayPreferences(payload),
|
||||
};
|
||||
|
||||
app.profileController = new ProfileModalController({
|
||||
|
||||
@@ -76,6 +76,7 @@ export class AppShell {
|
||||
threadState = "idle",
|
||||
threadErrorType = "",
|
||||
composerState = "idle",
|
||||
privacyMode = "nip04",
|
||||
notifications = [],
|
||||
signingAdapter = null,
|
||||
onSelectConversation,
|
||||
@@ -120,12 +121,14 @@ export class AppShell {
|
||||
messages,
|
||||
state: threadState,
|
||||
errorType: threadErrorType,
|
||||
privacyMode,
|
||||
}),
|
||||
);
|
||||
main.appendChild(
|
||||
Composer({
|
||||
document: doc,
|
||||
state: composerState,
|
||||
privacyMode,
|
||||
signingAdapter,
|
||||
onSend: onSendMessage,
|
||||
}),
|
||||
|
||||
+14
-8
@@ -13,7 +13,7 @@ export function Composer({
|
||||
document: doc,
|
||||
placeholder = "Write a message…",
|
||||
state = "idle",
|
||||
privacyMode: initialPrivacyMode = "standard",
|
||||
privacyMode: initialPrivacyMode = "nip04",
|
||||
signingAdapter = null,
|
||||
onSend,
|
||||
} = {}) {
|
||||
@@ -60,7 +60,7 @@ export function Composer({
|
||||
doc,
|
||||
"button",
|
||||
"dm-composer__privacy",
|
||||
"Standard",
|
||||
"NIP-04",
|
||||
);
|
||||
privacyToggle.type = "button";
|
||||
privacyToggle.setAttribute("aria-pressed", "false");
|
||||
@@ -121,17 +121,23 @@ export function Composer({
|
||||
});
|
||||
|
||||
const updatePrivacyLabel = () => {
|
||||
privacyToggle.textContent = privacyMode === "private" ? "Private" : "Standard";
|
||||
privacyToggle.setAttribute(
|
||||
"aria-pressed",
|
||||
privacyMode === "private" ? "true" : "false",
|
||||
);
|
||||
const normalized =
|
||||
typeof privacyMode === "string" ? privacyMode.trim().toLowerCase() : "";
|
||||
const isNip17 = normalized === "nip17" || normalized === "private";
|
||||
privacyToggle.textContent = isNip17 ? "NIP-17" : "NIP-04";
|
||||
privacyToggle.setAttribute("aria-pressed", isNip17 ? "true" : "false");
|
||||
privacyToggle.title = isNip17
|
||||
? "NIP-17 gift-wraps your DM so relays only see the wrapper and relay hints."
|
||||
: "NIP-04 sends a direct encrypted DM; relays can still see sender and recipient metadata.";
|
||||
};
|
||||
|
||||
updatePrivacyLabel();
|
||||
|
||||
privacyToggle.addEventListener("click", () => {
|
||||
privacyMode = privacyMode === "private" ? "standard" : "private";
|
||||
const normalized =
|
||||
typeof privacyMode === "string" ? privacyMode.trim().toLowerCase() : "";
|
||||
const isNip17 = normalized === "nip17" || normalized === "private";
|
||||
privacyMode = isNip17 ? "nip04" : "nip17";
|
||||
updatePrivacyLabel();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function MessageThread({
|
||||
messages = [],
|
||||
state = "idle",
|
||||
errorType = "",
|
||||
privacyMode = "nip04",
|
||||
} = {}) {
|
||||
if (!doc) {
|
||||
throw new Error("MessageThread requires a document reference.");
|
||||
@@ -39,6 +40,21 @@ export function MessageThread({
|
||||
createElement(doc, "span", "dm-message-thread__status", contact.status),
|
||||
);
|
||||
}
|
||||
const normalizedPrivacy =
|
||||
typeof privacyMode === "string" ? privacyMode.trim().toLowerCase() : "";
|
||||
const isNip17 = normalizedPrivacy === "nip17" || normalizedPrivacy === "private";
|
||||
const privacyLabel = `Privacy: ${isNip17 ? "NIP-17" : "NIP-04"}`;
|
||||
const privacyHint = isNip17
|
||||
? "NIP-17 gift-wraps your DM so relays only see the wrapper and relay hints."
|
||||
: "NIP-04 sends a direct encrypted DM; relays can still see sender and recipient metadata.";
|
||||
const privacyBadge = createElement(
|
||||
doc,
|
||||
"span",
|
||||
"dm-message-thread__status",
|
||||
privacyLabel,
|
||||
);
|
||||
privacyBadge.title = privacyHint;
|
||||
header.appendChild(privacyBadge);
|
||||
root.appendChild(header);
|
||||
|
||||
const timeline = createElement(doc, "div", "dm-message-thread__timeline");
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
formatHashtag,
|
||||
} from "../utils/hashtagNormalization.js";
|
||||
import { getActiveSigner } from "../nostr/client.js";
|
||||
import { sanitizeRelayList } from "../nostr/nip46Client.js";
|
||||
import { buildPublicUrl, buildR2Key } from "../r2.js";
|
||||
import { buildProfileMetadataEvent } from "../nostrEventSchemas.js";
|
||||
|
||||
@@ -758,6 +759,40 @@ const STATE_CONTRACT = [
|
||||
return stored;
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "getDmRelayPreferences",
|
||||
type: "function",
|
||||
description:
|
||||
"Returns the cached DM relay hints for a given pubkey (publisher view).",
|
||||
fallback: (internal) => (pubkey) => {
|
||||
const normalized =
|
||||
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
|
||||
if (!normalized || !(internal.dmRelayHints instanceof Map)) {
|
||||
return [];
|
||||
}
|
||||
const hints = internal.dmRelayHints.get(normalized);
|
||||
return Array.isArray(hints) ? hints.slice() : [];
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "setDmRelayPreferences",
|
||||
type: "function",
|
||||
description:
|
||||
"Stores DM relay hints for a given pubkey (publisher view).",
|
||||
fallback: (internal) => (pubkey, hints = []) => {
|
||||
if (!(internal.dmRelayHints instanceof Map)) {
|
||||
internal.dmRelayHints = new Map();
|
||||
}
|
||||
const normalized =
|
||||
typeof pubkey === "string" ? pubkey.trim().toLowerCase() : "";
|
||||
if (!normalized) {
|
||||
return [];
|
||||
}
|
||||
const stored = Array.isArray(hints) ? hints.slice() : [];
|
||||
internal.dmRelayHints.set(normalized, stored);
|
||||
return stored;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function buildServicesContract(services = {}, internalState) {
|
||||
@@ -1089,6 +1124,7 @@ export class ProfileModalController {
|
||||
onSendDm: callbacks.onSendDm || noop,
|
||||
onTogglePrivacy: callbacks.onTogglePrivacy || noop,
|
||||
onOpenRelays: callbacks.onOpenRelays || noop,
|
||||
onPublishDmRelayPreferences: callbacks.onPublishDmRelayPreferences || noop,
|
||||
};
|
||||
|
||||
this.profileModal = null;
|
||||
@@ -1175,6 +1211,12 @@ export class ProfileModalController {
|
||||
this.profileMessagesSendDmButton = null;
|
||||
this.profileMessagesOpenRelaysButton = null;
|
||||
this.profileMessagesPrivacyToggle = null;
|
||||
this.profileMessagesPrivacyMode = null;
|
||||
this.profileMessagesRelayList = null;
|
||||
this.profileMessagesRelayInput = null;
|
||||
this.profileMessagesRelayAddButton = null;
|
||||
this.profileMessagesRelayPublishButton = null;
|
||||
this.profileMessagesRelayStatus = null;
|
||||
this.walletUriInput = null;
|
||||
this.walletDefaultZapInput = null;
|
||||
this.walletSaveButton = null;
|
||||
@@ -1443,6 +1485,18 @@ export class ProfileModalController {
|
||||
document.getElementById("profileMessagesOpenRelays") || null;
|
||||
this.profileMessagesPrivacyToggle =
|
||||
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;
|
||||
|
||||
if (this.pendingMessagesRender) {
|
||||
const { messages, actorPubkey } = this.pendingMessagesRender;
|
||||
@@ -1469,6 +1523,8 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
this.setMessagesLoadingState(this.messagesLoadingState || "idle");
|
||||
this.updateMessagePrivacyModeDisplay();
|
||||
this.populateDmRelayPreferences();
|
||||
|
||||
this.walletUriInput = document.getElementById("profileWalletUri") || null;
|
||||
this.walletDefaultZapInput =
|
||||
@@ -1850,6 +1906,235 @@ export class ProfileModalController {
|
||||
return null;
|
||||
}
|
||||
|
||||
resolveActiveDmRelayOwner() {
|
||||
const active = this.normalizeHexPubkey(this.getActivePubkey());
|
||||
if (active) {
|
||||
return active;
|
||||
}
|
||||
|
||||
return this.resolveActiveDmActor();
|
||||
}
|
||||
|
||||
getActiveDmRelayPreferences() {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner || typeof this.state.getDmRelayPreferences !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const relays = this.state.getDmRelayPreferences(owner);
|
||||
return Array.isArray(relays) ? relays.slice() : [];
|
||||
}
|
||||
|
||||
setActiveDmRelayPreferences(relays = []) {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner || typeof this.state.setDmRelayPreferences !== "function") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.state.setDmRelayPreferences(owner, relays);
|
||||
}
|
||||
|
||||
setDmRelayPreferencesStatus(message = "") {
|
||||
if (!(this.profileMessagesRelayStatus instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const text = typeof message === "string" ? message.trim() : "";
|
||||
this.profileMessagesRelayStatus.textContent = text;
|
||||
}
|
||||
|
||||
updateMessagePrivacyModeDisplay() {
|
||||
if (!(this.profileMessagesPrivacyMode instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isNip17 =
|
||||
this.profileMessagesPrivacyToggle instanceof HTMLInputElement
|
||||
? this.profileMessagesPrivacyToggle.checked
|
||||
: false;
|
||||
const label = isNip17 ? "NIP-17" : "NIP-04";
|
||||
this.profileMessagesPrivacyMode.textContent = `Privacy: ${label}`;
|
||||
this.profileMessagesPrivacyMode.title = isNip17
|
||||
? "NIP-17 gift-wraps your DM so relays only see the wrapper and relay hints."
|
||||
: "NIP-04 sends a direct encrypted DM; relays can still see sender and recipient metadata.";
|
||||
}
|
||||
|
||||
async refreshDmRelayPreferences({ force = false } = {}) {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.populateDmRelayPreferences();
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = this.getActiveDmRelayPreferences();
|
||||
if (!existing.length || force) {
|
||||
if (typeof this.services.fetchDmRelayHints === "function") {
|
||||
try {
|
||||
const hints = await this.services.fetchDmRelayHints(owner);
|
||||
if (typeof this.state.setDmRelayPreferences === "function") {
|
||||
this.state.setDmRelayPreferences(owner, hints);
|
||||
}
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to refresh DM relay hints for profile:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.populateDmRelayPreferences();
|
||||
}
|
||||
|
||||
populateDmRelayPreferences() {
|
||||
if (!(this.profileMessagesRelayList instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
const relays = this.getActiveDmRelayPreferences();
|
||||
|
||||
this.profileMessagesRelayList.innerHTML = "";
|
||||
|
||||
if (!owner) {
|
||||
const emptyState = document.createElement("li");
|
||||
emptyState.className =
|
||||
"card border border-dashed border-border/60 p-4 text-center text-xs text-muted";
|
||||
emptyState.textContent = "Sign in to add DM relay hints.";
|
||||
this.profileMessagesRelayList.appendChild(emptyState);
|
||||
this.setDmRelayPreferencesStatus("");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!relays.length) {
|
||||
const emptyState = document.createElement("li");
|
||||
emptyState.className =
|
||||
"card border border-dashed border-border/60 p-4 text-center text-xs text-muted";
|
||||
emptyState.textContent = "No DM relay hints yet.";
|
||||
this.profileMessagesRelayList.appendChild(emptyState);
|
||||
return;
|
||||
}
|
||||
|
||||
relays.forEach((url) => {
|
||||
const item = document.createElement("li");
|
||||
item.className = "card flex items-center justify-between gap-3 p-3";
|
||||
|
||||
const label = document.createElement("p");
|
||||
label.className = "text-xs font-medium text-text break-all";
|
||||
label.textContent = url;
|
||||
|
||||
const removeBtn = document.createElement("button");
|
||||
removeBtn.type = "button";
|
||||
removeBtn.className = "btn-ghost focus-ring text-xs";
|
||||
removeBtn.dataset.variant = "danger";
|
||||
removeBtn.textContent = "Remove";
|
||||
removeBtn.addEventListener("click", () => {
|
||||
void this.handleRemoveDmRelayPreference(url);
|
||||
});
|
||||
|
||||
item.appendChild(label);
|
||||
item.appendChild(removeBtn);
|
||||
this.profileMessagesRelayList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
async handleAddDmRelayPreference() {
|
||||
const input = this.profileMessagesRelayInput;
|
||||
if (!(input instanceof HTMLInputElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.showError("Please sign in to save DM relay hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
const rawValue = typeof input.value === "string" ? input.value.trim() : "";
|
||||
const sanitized = sanitizeRelayList([rawValue]);
|
||||
const relayUrl = sanitized[0];
|
||||
if (!relayUrl) {
|
||||
this.showError("Enter a valid WSS relay URL.");
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.getActiveDmRelayPreferences();
|
||||
const next = sanitizeRelayList([...current, relayUrl]);
|
||||
this.setActiveDmRelayPreferences(next);
|
||||
input.value = "";
|
||||
this.populateDmRelayPreferences();
|
||||
this.setDmRelayPreferencesStatus("DM relay hint added.");
|
||||
}
|
||||
|
||||
async handleRemoveDmRelayPreference(url) {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.showError("Please sign in to update DM relay hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
const target = typeof url === "string" ? url.trim() : "";
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const current = this.getActiveDmRelayPreferences();
|
||||
const next = current.filter((entry) => entry !== target);
|
||||
this.setActiveDmRelayPreferences(next);
|
||||
this.populateDmRelayPreferences();
|
||||
this.setDmRelayPreferencesStatus("DM relay hint removed.");
|
||||
}
|
||||
|
||||
async handlePublishDmRelayPreferences() {
|
||||
const owner = this.resolveActiveDmRelayOwner();
|
||||
if (!owner) {
|
||||
this.showError("Please sign in to publish DM relay hints.");
|
||||
return;
|
||||
}
|
||||
|
||||
const relays = this.getActiveDmRelayPreferences();
|
||||
if (!relays.length) {
|
||||
this.showError("Add at least one DM relay before publishing.");
|
||||
return;
|
||||
}
|
||||
|
||||
const callback = this.callbacks.onPublishDmRelayPreferences;
|
||||
if (!callback || callback === noop) {
|
||||
this.showError("DM relay publishing is unavailable right now.");
|
||||
return;
|
||||
}
|
||||
|
||||
this.setDmRelayPreferencesStatus("Publishing DM relay hints…");
|
||||
|
||||
try {
|
||||
const result = await callback({
|
||||
pubkey: owner,
|
||||
relays,
|
||||
controller: this,
|
||||
});
|
||||
if (result?.ok) {
|
||||
const acceptedCount = Array.isArray(result.accepted)
|
||||
? result.accepted.length
|
||||
: 0;
|
||||
const summary = acceptedCount
|
||||
? `Published to ${acceptedCount} relay${acceptedCount === 1 ? "" : "s"}.`
|
||||
: "DM relay hints published.";
|
||||
this.showSuccess("DM relay hints published.");
|
||||
this.setDmRelayPreferencesStatus(summary);
|
||||
return;
|
||||
}
|
||||
this.showError("Failed to publish DM relay hints.");
|
||||
this.setDmRelayPreferencesStatus("DM relay hints publish failed.");
|
||||
} catch (error) {
|
||||
const message =
|
||||
error && typeof error.message === "string" && error.message.trim()
|
||||
? error.message.trim()
|
||||
: "Failed to publish DM relay hints.";
|
||||
this.showError(message);
|
||||
this.setDmRelayPreferencesStatus(message);
|
||||
}
|
||||
}
|
||||
|
||||
buildDmRecipientContext(pubkey) {
|
||||
const normalized = this.normalizeHexPubkey(pubkey);
|
||||
if (!normalized) {
|
||||
@@ -1996,6 +2281,7 @@ export class ProfileModalController {
|
||||
if (this.profileMessagesPrivacyToggle instanceof HTMLInputElement) {
|
||||
this.profileMessagesPrivacyToggle.checked = Boolean(enabled);
|
||||
}
|
||||
this.updateMessagePrivacyModeDisplay();
|
||||
}
|
||||
|
||||
async ensureDirectMessageSubscription(actorPubkey = null) {
|
||||
@@ -2098,6 +2384,8 @@ export class ProfileModalController {
|
||||
if (!normalized) {
|
||||
this.setMessagesLoadingState("unauthenticated");
|
||||
this.updateMessagesReloadState();
|
||||
this.populateDmRelayPreferences();
|
||||
this.setDmRelayPreferencesStatus("");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2111,6 +2399,8 @@ export class ProfileModalController {
|
||||
reason: "identity-change",
|
||||
});
|
||||
}
|
||||
|
||||
void this.refreshDmRelayPreferences({ force: true });
|
||||
}
|
||||
|
||||
setMessagesLoadingState(state, options = {}) {
|
||||
@@ -2256,6 +2546,8 @@ export class ProfileModalController {
|
||||
helper.setAttribute("hidden", "");
|
||||
}
|
||||
}
|
||||
|
||||
this.updateMessagePrivacyModeDisplay();
|
||||
}
|
||||
|
||||
setMessagesAnnouncement(message) {
|
||||
@@ -2338,6 +2630,7 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
this.dmPrivacyToggleTouched = true;
|
||||
this.updateMessagePrivacyModeDisplay();
|
||||
const callback = this.callbacks.onTogglePrivacy;
|
||||
if (callback && callback !== noop) {
|
||||
callback({
|
||||
@@ -2413,6 +2706,7 @@ export class ProfileModalController {
|
||||
this.profileMessagesPrivacyToggle instanceof HTMLInputElement
|
||||
? this.profileMessagesPrivacyToggle.checked
|
||||
: false;
|
||||
const senderRelayHints = this.getActiveDmRelayPreferences();
|
||||
|
||||
if (useNip17 && !recipientRelayHints.length) {
|
||||
this.showStatus(
|
||||
@@ -2443,6 +2737,7 @@ export class ProfileModalController {
|
||||
? {
|
||||
useNip17: true,
|
||||
recipientRelayHints,
|
||||
senderRelayHints,
|
||||
}
|
||||
: {},
|
||||
);
|
||||
@@ -3198,6 +3493,27 @@ 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();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileMessageSendButton instanceof HTMLElement) {
|
||||
this.profileMessageSendButton.addEventListener("click", () => {
|
||||
void this.handleSendProfileMessage();
|
||||
@@ -4694,6 +5010,7 @@ export class ProfileModalController {
|
||||
} else if (target === "messages") {
|
||||
this.resumeProfileMessages();
|
||||
void this.populateProfileMessages({ reason: "pane-select" });
|
||||
void this.refreshDmRelayPreferences();
|
||||
} else if (target === "wallet") {
|
||||
this.refreshWalletPaneState();
|
||||
} else if (target === "storage") {
|
||||
@@ -10331,6 +10648,7 @@ export class ProfileModalController {
|
||||
this.refreshWalletPaneState();
|
||||
this.populateHashtagPreferences();
|
||||
this.handleActiveDmIdentityChanged(activePubkey);
|
||||
void this.refreshDmRelayPreferences({ force: true });
|
||||
|
||||
postLoginPromise
|
||||
.then(() => {
|
||||
@@ -10340,6 +10658,7 @@ export class ProfileModalController {
|
||||
this.populateProfileRelays();
|
||||
this.refreshWalletPaneState();
|
||||
this.populateHashtagPreferences();
|
||||
void this.refreshDmRelayPreferences({ force: true });
|
||||
})
|
||||
.catch((error) => {
|
||||
userLogger.warn(
|
||||
@@ -10407,6 +10726,8 @@ export class ProfileModalController {
|
||||
this.clearHashtagInputs();
|
||||
this.setHashtagStatus("", "muted");
|
||||
this.handleActiveDmIdentityChanged(null);
|
||||
this.populateDmRelayPreferences();
|
||||
this.setDmRelayPreferencesStatus("");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user