From cbbd98ae5446b11f2a432dee7ffea291cdf2ee21 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 1 Mar 2026 21:09:04 -0800 Subject: messages --- src/components/Menu.svelte | 2 +- src/lib/nip17.ts | 44 +- .../(app)/[username]/(profile)/+layout.svelte | 2 +- src/routes/(app)/dm/+page.svelte | 694 --------------------- src/routes/(app)/messages/+layout.svelte | 56 ++ src/routes/(app)/messages/+page.server.ts | 43 -- src/routes/(app)/messages/+page.svelte | 212 ++++--- src/routes/(app)/messages/[pubkey]/+page.svelte | 394 ++++++++++++ .../(app)/messages/[recipient]/+page.server.ts | 22 - src/routes/(app)/messages/[recipient]/+page.svelte | 189 ------ src/routes/(static)/register/auth/+page.svelte | 24 +- 11 files changed, 648 insertions(+), 1034 deletions(-) delete mode 100644 src/routes/(app)/dm/+page.svelte create mode 100644 src/routes/(app)/messages/+layout.svelte delete mode 100644 src/routes/(app)/messages/+page.server.ts create mode 100644 src/routes/(app)/messages/[pubkey]/+page.svelte delete mode 100644 src/routes/(app)/messages/[recipient]/+page.server.ts delete mode 100644 src/routes/(app)/messages/[recipient]/+page.svelte (limited to 'src') diff --git a/src/components/Menu.svelte b/src/components/Menu.svelte index 17c0bf08..6c581ba1 100644 --- a/src/components/Menu.svelte +++ b/src/components/Menu.svelte @@ -16,7 +16,7 @@ // icon: "ph:storefront-bold", // href: `https://coinosmerch.com`, // }, - // { key: "nav.dm", icon: "ph:chat-bold", href: `/dm` }, + { key: "nav.dm", icon: "ph:chat-bold", href: `/messages` }, { key: "nav.dark", icon: "ph:moon-stars-bold", href: `/dark` }, { key: "nav.signOut", icon: "ph:sign-out-bold", href: `/logout` }, ]; diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts index 1400b5a5..69f69464 100644 --- a/src/lib/nip17.ts +++ b/src/lib/nip17.ts @@ -162,10 +162,10 @@ export const decrypt = async (wrapped: any, user: any): Promise => { } }; -// Returns the rumours of the messages the provided user has sent or received. -// If the events have an expiration timestamp, -// that timestamp is added to its rumour object as the property `expiration`. -export const getMessageRumours = async (user: any): Promise => { +// In-memory cache for rumours, keyed by pubkey +let rumourCache = new Map(); + +const fetchMessageRumours = async (user: any): Promise => { const preferredRelays = await getPreferredRelays(user.pubkey); // Always include default relays to catch messages that might be there const relays = Array.from(new Set([...preferredRelays, ...DM_RELAYS_LIST])); @@ -197,9 +197,45 @@ export const getMessageRumours = async (user: any): Promise => { } } + rumourCache.set(user.pubkey, rumours); return rumours; }; +// Returns the rumours of the messages the provided user has sent or received. +// If the events have an expiration timestamp, +// that timestamp is added to its rumour object as the property `expiration`. +// Returns cached results immediately if available, and refreshes in the background. +export const getMessageRumours = async (user: any): Promise => { + const cached = rumourCache.get(user.pubkey); + if (cached) { + fetchMessageRumours(user); + return cached; + } + return fetchMessageRumours(user); +}; + +// Cache for computed chat lists and user info +let chatListCache = new Map(); +let userInfoCache = new Map(); + +export const getCachedChatList = (userPubkey: string) => chatListCache.get(userPubkey); +export const setCachedChatList = (userPubkey: string, chats: any[], rumours: any[]) => { + chatListCache.set(userPubkey, { chats, rumours }); +}; + +export const getCachedUserInfo = (pubkey: string) => userInfoCache.get(pubkey); +export const setCachedUserInfo = (pubkey: string, info: any) => { + userInfoCache.set(pubkey, info); +}; + +// Add a rumour to the cache (used by subscription handlers) +export const cacheRumour = (userPubkey: string, rumour: any) => { + const cached = rumourCache.get(userPubkey); + if (cached && !cached.find((r: any) => r.id === rumour.id)) { + cached.push(rumour); + } +}; + // Returns the preferred relays of the user with the provided pubkey, // according to their kind-10050 event. export const getPreferredRelays = async (pubkey: string): Promise => { diff --git a/src/routes/(app)/[username]/(profile)/+layout.svelte b/src/routes/(app)/[username]/(profile)/+layout.svelte index df63051f..55fffb71 100644 --- a/src/routes/(app)/[username]/(profile)/+layout.svelte +++ b/src/routes/(app)/[username]/(profile)/+layout.svelte @@ -40,7 +40,7 @@ {:else} - + {/if} diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte deleted file mode 100644 index 1453ccd6..00000000 --- a/src/routes/(app)/dm/+page.svelte +++ /dev/null @@ -1,694 +0,0 @@ - - -
-
-

- {creatingNewChat ? $t("dm.newChatHeader") : $t("dm.chatHeader")} - -

- {#if creatingNewChat} - - {/if} - {#if !creatingNewChat} - {#each chats as c} - - {/each} - {:else if searchQuery !== ""} - - {#if searchQuery.includes(".")} - - {/if} - {#if searchQuery.length === 64 || searchQuery.slice(0, 4) === "npub"} - - {/if} - {/if} -
-
-

- {$t("dm.header")} -

- - {#if selectedChat} -
- {#each dates as day} -

{formatDate(new Date(day * 86400 * 1000))}

-
    - {#each messageRumours.get(day) as rumour, i} - {@const dayMessages = messageRumours.get(day)} - {@const next = dayMessages[i + 1]} - {@const isLastInRun = !next || next.pubkey !== rumour.pubkey} - {#if rumour.pubkey === user.pubkey && pTagKeys(rumour).includes(selectedChat.pubkey)} -
  • -
    - {rumour.content} - - {TIME.format(new Date(rumour.created_at * 1000))} - -
    - {#if isLastInRun && user.picture} - {user.username} - {:else} -
    - {/if} -
  • - {:else if rumour.pubkey === selectedChat.pubkey && pTagKeys(rumour).includes(user.pubkey)} -
  • - {#if isLastInRun && selectedChat.picture} - {name(selectedChat)} - {:else} -
    - {/if} -
    - {rumour.content} - - {TIME.format(new Date(rumour.created_at * 1000))} - -
    -
  • - {/if} - {/each} -
- {/each} -
-
- -
- - sendMessage(text)} - /> - {#if sendError} -

{sendError}

- {/if} -
- {:else if chats.length == 0} -

{$t("dm.noChats")}

- {:else} -

{$t("dm.selectChat")}

- {/if} - {#if relayWarningShown} -

- - {$t("dm.noRelaysSet")} - {$t("dm.nostrSettingsLink")} - -

- {/if} -
-
- - diff --git a/src/routes/(app)/messages/+layout.svelte b/src/routes/(app)/messages/+layout.svelte new file mode 100644 index 00000000..7e7e3cd0 --- /dev/null +++ b/src/routes/(app)/messages/+layout.svelte @@ -0,0 +1,56 @@ + + + + + + +{@render children?.()} + + diff --git a/src/routes/(app)/messages/+page.server.ts b/src/routes/(app)/messages/+page.server.ts deleted file mode 100644 index 80d6f1a9..00000000 --- a/src/routes/(app)/messages/+page.server.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { get } from "$lib/utils"; -import { redirect } from "@sveltejs/kit"; - -interface Message { - created_at: number; -} - -interface Note { - created_at: number; - seen: number; -} - -export async function load({ parent, url }) { - const { user } = await parent(); - const username = url.searchParams.get("username"); - if (user.username !== username) redirect(307, `/${username}`); - const since = url.searchParams.get("since") || 0; - - let messages = []; - let notes = []; - - if (user?.pubkey) { - const { pubkey } = user; - try { - messages = await get(`/${pubkey}/${since}/messages`); - messages = messages.sort((a: Message, b: Message) => b.created_at - a.created_at); - } catch (e) { - console.log("failed to fetch nostr messages", e); - } - - try { - notes = await get(`/${pubkey}/notes`); - } catch (e) { - console.log(`failed to fetch nostr notes for ${pubkey}`, e); - } - - notes.map((e: Note) => { - e.seen = e.created_at; - }); - } - - return { messages, notes }; -} diff --git a/src/routes/(app)/messages/+page.svelte b/src/routes/(app)/messages/+page.svelte index a858df99..6eb5a399 100644 --- a/src/routes/(app)/messages/+page.svelte +++ b/src/routes/(app)/messages/+page.svelte @@ -1,94 +1,154 @@ -
-

- {$t("user.messages")} -

- {#if latest.length && user?.id === subject.id} -
- {#each latest as { content, pubkey, author, recipient }} - {@const u = author.id === user.id ? recipient : author} - -
-
- -
-
-
{u.username}
-
{content}
-
-
-
- {/each} -
+
+ {#if chats.length === 0} +

{$t("dm.noChats")}

{:else} -
{$t("user.noMessages")}
+ {#each chats as c} + + {#if c.picture} + {name(c)} + {:else} +
+ {/if} +
+
{name(c)}
+
{lastMessage(allRumours, c.pubkey)}
+
+
+ {/each} {/if}
diff --git a/src/routes/(app)/messages/[pubkey]/+page.svelte b/src/routes/(app)/messages/[pubkey]/+page.svelte new file mode 100644 index 00000000..0efa6206 --- /dev/null +++ b/src/routes/(app)/messages/[pubkey]/+page.svelte @@ -0,0 +1,394 @@ + + +
+ {#if selectedChat} + {@const chatProfile = selectedChat.coinosUsername ? `/${selectedChat.coinosUsername}` : null} + + {#if selectedChat.picture} + {name(selectedChat)} + {/if} +

{name(selectedChat)}

+
+ +
+ {#each dates as day} +

{formatDate(new Date(day * 86400 * 1000))}

+
    + {#each messageRumours.get(day) ?? [] as rumour, i} + {@const dayMessages = messageRumours.get(day) ?? []} + {@const next = dayMessages[i + 1]} + {@const isLastInRun = !next || next.pubkey !== rumour.pubkey} + {#if rumour.pubkey === user.pubkey && pTagKeys(rumour).includes(selectedChat.pubkey)} +
  • +
    + {rumour.content} + + {TIME.format(new Date(rumour.created_at * 1000))} + +
    + {#if isLastInRun && user.picture} + {user.username} + {:else} +
    + {/if} +
  • + {:else if rumour.pubkey === selectedChat.pubkey && pTagKeys(rumour).includes(user.pubkey)} +
  • + {#if isLastInRun && selectedChat.picture} + {name(selectedChat)} + {:else} +
    + {/if} +
    + {rumour.content} + + {TIME.format(new Date(rumour.created_at * 1000))} + +
    +
  • + {/if} + {/each} +
+ {/each} +
+
+ +
+ + sendMessage(text)} + /> + {#if sendError} +

{sendError}

+ {/if} +
+ {/if} + + {#if relayWarningShown} +

+ + {$t("dm.noRelaysSet")} + {$t("dm.nostrSettingsLink")} + +

+ {/if} +
+ + diff --git a/src/routes/(app)/messages/[recipient]/+page.server.ts b/src/routes/(app)/messages/[recipient]/+page.server.ts deleted file mode 100644 index feb0d86f..00000000 --- a/src/routes/(app)/messages/[recipient]/+page.server.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { get } from "$lib/utils"; -import type { Message } from "$lib/types"; - -export async function load({ params, parent, url }) { - const { user } = await parent(); - const since = url.searchParams.get("since") || 0; - - let messages: Message[] = []; - const recipient = await get(`/users/${params.recipient}`); - - try { - messages = await get(`/${user.pubkey}/${since}/messages`); - - messages = messages - .filter((m: Message) => m.recipient?.id === recipient.id || m.author?.id === recipient.id) - .sort((a, b) => a.created_at - b.created_at); - } catch (e) { - console.log("failed to fetch nostr messages", e); - } - - return { messages, recipient, user }; -} diff --git a/src/routes/(app)/messages/[recipient]/+page.svelte b/src/routes/(app)/messages/[recipient]/+page.svelte deleted file mode 100644 index 29c4478a..00000000 --- a/src/routes/(app)/messages/[recipient]/+page.svelte +++ /dev/null @@ -1,189 +0,0 @@ - - -
- - -
- {#each messages as { id, author, message, created_at, pubkey }} - {@const ours = pubkey === user.pubkey} - {@const theirs = !ours} - - {#if message} -
-
- {message} -
-
- -
-
-
- {format(new Date(created_at * 1000), "MMM d")}, {format(new Date(created_at * 1000), "h:mm aa")} -
- {/if} - {/each} -
- -
- - - -
- -
-
- - diff --git a/src/routes/(static)/register/auth/+page.svelte b/src/routes/(static)/register/auth/+page.svelte index afb39953..232f4290 100644 --- a/src/routes/(static)/register/auth/+page.svelte +++ b/src/routes/(static)/register/auth/+page.svelte @@ -186,6 +186,7 @@ let passkeyRegister = async () => { passkeyLoading = true; + let passkeyToken: string | null = null; try { let pw = Array.from(crypto.getRandomValues(new Uint8Array(32)), (b) => b.toString(16).padStart(2, "0"), @@ -212,8 +213,10 @@ return; } - const { token, sk } = result.data; - const { prfKey, credentialId } = await registerPasskey(token); + passkeyToken = result.data.token; + const passkeySk = result.data.sk; + + const { prfKey, credentialId } = await registerPasskey(passkeyToken!); // Generate canonical key and derive nostr key before activate // so we can pass the pubkey to the server-side activate action @@ -230,9 +233,9 @@ } const activateData = new FormData(); - activateData.append("token", token); + activateData.append("token", passkeyToken!); activateData.append("username", username); - if (sk) activateData.append("sk", sk); + if (passkeySk) activateData.append("sk", passkeySk); if (nostrPubkey) activateData.append("pubkey", nostrPubkey); activateData.append("loginRedirect", (redirect ?? "") as string); @@ -261,6 +264,19 @@ applyAction(activateResult); } catch (e: any) { + // Clean up the account created by passkeyCreate if passkey registration failed + if (passkeyToken) { + try { + await fetch("/post/user/delete", { + method: "POST", + headers: { + "Content-Type": "application/json", + authorization: `Bearer ${passkeyToken}`, + }, + body: JSON.stringify({}), + }); + } catch {} + } if (e.name !== "NotAllowedError") { fail(e.message || "Passkey registration failed"); } -- cgit v1.2.3