From c7e2383736d3ef99f069226bbafad9fbddfeb030 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Sun, 1 Mar 2026 18:51:16 -0800 Subject: messages ui --- src/routes/(app)/+layout.svelte | 3 + src/routes/(app)/dm/+page.svelte | 371 +++++++++++++------------ src/routes/(static)/register/auth/+page.svelte | 38 ++- 3 files changed, 238 insertions(+), 174 deletions(-) (limited to 'src/routes') diff --git a/src/routes/(app)/+layout.svelte b/src/routes/(app)/+layout.svelte index e06ec990..40f28444 100644 --- a/src/routes/(app)/+layout.svelte +++ b/src/routes/(app)/+layout.svelte @@ -185,6 +185,9 @@ // Auto-unlock ark wallet if silent entropy is available if (!$arkkey) autoUnlockArk(); + + // Auto-unlock nostr key if silent entropy is available + import("$lib/seed").then(({ autoUnlockNostr }) => autoUnlockNostr(user.pubkey)); } // if (window.NDEFReader) { diff --git a/src/routes/(app)/dm/+page.svelte b/src/routes/(app)/dm/+page.svelte index b7026f94..1453ccd6 100644 --- a/src/routes/(app)/dm/+page.svelte +++ b/src/routes/(app)/dm/+page.svelte @@ -1,5 +1,5 @@
@@ -346,23 +374,21 @@ {/if} {#if !creatingNewChat} {#each chats as c} - {#if (selectedChat && selectedChat.pubkey === c.pubkey) || !(muted && muted.has(c.pubkey))} - - {/if} + {/each} {:else if searchQuery !== ""} - - - {#if !(muted && muted.has(selectedChat.pubkey))} -
- {#each dates as day} -

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

-
    - {#each messageRumours.get(day) as rumour} - {#if rumour.pubkey === user.pubkey && pTagKeys(rumour).includes(selectedChat.pubkey)} -
  • + {#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))} -
    • - {:else if rumour.pubkey === selectedChat.pubkey && pTagKeys(rumour).includes(user.pubkey)} -
    • + {#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} -
- - +
+ + {/if} + {/each} + + {/each} +
+ + +
+ {sendError}

{/if} - {/if} - - {#if !(muted && muted.has(selectedChat.pubkey))} - - - : - - - . -

{$t("dm.fadedWarning")}

- {#if canSend && !canSendExpiring} -

- {$t("dm.cantSendExpiring").replace("[R]", name(selectedChat))} -

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

{$t("dm.noChats")}

{:else} @@ -529,27 +525,11 @@ text-decoration-line: underline; } - .inline { - white-space: nowrap; - } - .short { height: 1.5em; margin-bottom: 10px; } - .small-width { - width: 6em; - } - - .tiny { - width: 2em; - } - - .invalid { - text-decoration: line-through; - } - .super-container { display: flex; } @@ -581,12 +561,17 @@ .tall-btn { min-height: 3em; width: 100%; + display: flex; + align-items: center; + gap: 0.5rem; } - .small-btn { - padding: 0 15px; - border-radius: 5px; - text-align: left; + .sidebar-avatar { + width: 32px; + height: 32px; + border-radius: 9999px; + object-fit: cover; + flex-shrink: 0; } .push-right { @@ -597,6 +582,35 @@ margin: 1em 15px 0.5em; } + .message-row { + display: flex; + align-items: flex-end; + gap: 0.5rem; + margin: 3px 0; + } + + .message-row.by-user { + justify-content: flex-end; + } + + .message-row.by-other { + justify-content: flex-start; + } + + .msg-avatar { + width: 28px; + height: 28px; + border-radius: 9999px; + object-fit: cover; + flex-shrink: 0; + } + + .msg-avatar-placeholder { + width: 28px; + height: 28px; + flex-shrink: 0; + } + .light-chat-btn { background-color: #eeeeee; } @@ -636,6 +650,25 @@ text-align: right; } + #messages { + overflow-y: auto; + max-height: 50vh; + scrollbar-width: thin; + } + + #messages::-webkit-scrollbar { + width: 6px; + } + + #messages::-webkit-scrollbar-thumb { + background: rgba(128, 128, 128, 0.3); + border-radius: 3px; + } + + #messages::-webkit-scrollbar-button { + display: none; + } + .message { padding: 0.25em 0.75em; width: fit-content; @@ -651,15 +684,11 @@ background-color: #222; } - .by-user { - margin: 3px 0px 3px auto; - } - - .by-other { - margin: 3px auto 3px 0px; + .input-bar { + padding: 0.5rem 0; + display: flex; + flex-direction: column; + gap: 0.5rem; } - .vcenter { - margin: auto 0px; - } diff --git a/src/routes/(static)/register/auth/+page.svelte b/src/routes/(static)/register/auth/+page.svelte index 32539079..afb39953 100644 --- a/src/routes/(static)/register/auth/+page.svelte +++ b/src/routes/(static)/register/auth/+page.svelte @@ -13,7 +13,7 @@ import PasswordInput from "$comp/PasswordInput.svelte"; import Spinner from "$comp/Spinner.svelte"; - import { fail, focus } from "$lib/utils"; + import { fail, focus, post } from "$lib/utils"; import { registerPasskey } from "$lib/passkey"; import { rememberPrfKey, defaultRememberForMs } from "$lib/passwordCache"; import { avatar, signer, password, pin, loginRedirect } from "$lib/store"; @@ -158,8 +158,21 @@ const { sk } = await deriveAuthKeypair(user.username, user.password); const canonicalKey = generateCanonicalKey(); const wrapped = await wrapCanonicalKey(sk.buffer as ArrayBuffer, canonicalKey); - await saveEncryptedKeys({ [passwordMethodId()]: wrapped }); rememberPrfKey(canonicalKey, defaultRememberForMs); + + const encryptedKeys = { [passwordMethodId()]: wrapped }; + let nostrPubkey: string | undefined; + try { + const { deriveNostrKey } = await import("$lib/seed"); + const { nip19 } = await import("nostr-tools"); + const nostr = await deriveNostrKey(canonicalKey); + localStorage.setItem("nsec", nip19.nsecEncode(nostr.sk)); + nostrPubkey = nostr.pubkey; + } catch (e) { + console.log("Nostr key derivation failed", e); + } + + await post("/post/user", nostrPubkey ? { encryptedKeys, pubkey: nostrPubkey } : { encryptedKeys }); } catch (e) { console.log("Canonical key generation failed", e); } @@ -202,10 +215,25 @@ const { token, sk } = result.data; const { prfKey, credentialId } = await registerPasskey(token); + // Generate canonical key and derive nostr key before activate + // so we can pass the pubkey to the server-side activate action + const canonicalKey = generateCanonicalKey(); + let nostrPubkey: string | undefined; + let nostrSk: Uint8Array | undefined; + try { + const { deriveNostrKey } = await import("$lib/seed"); + const nostr = await deriveNostrKey(canonicalKey); + nostrPubkey = nostr.pubkey; + nostrSk = nostr.sk; + } catch (e) { + console.log("Nostr key derivation failed", e); + } + const activateData = new FormData(); activateData.append("token", token); activateData.append("username", username); if (sk) activateData.append("sk", sk); + if (nostrPubkey) activateData.append("pubkey", nostrPubkey); activateData.append("loginRedirect", (redirect ?? "") as string); const activateResponse = await fetch("/register/auth?/activate", { @@ -217,10 +245,14 @@ if (activateResult.type === "success" || activateResult.type === "redirect") { try { - const canonicalKey = generateCanonicalKey(); const wrapped = await wrapCanonicalKey(prfKey, canonicalKey); await saveEncryptedKeys({ [passkeyMethodId(credentialId)]: wrapped }); rememberPrfKey(canonicalKey, defaultRememberForMs); + + if (nostrSk) { + const { nip19 } = await import("nostr-tools"); + localStorage.setItem("nsec", nip19.nsecEncode(nostrSk)); + } } catch (e) { console.log("Canonical key generation failed", e); } -- cgit v1.2.3