From 47316e28150953909c71adfd1e9100c4e08024b1 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 6 Mar 2026 10:06:53 -0800 Subject: chat fixes --- src/routes/(app)/messages/+page.svelte | 55 +++++----- src/routes/(app)/messages/[id]/+page.svelte | 155 +++++++++++++++++++++------- 2 files changed, 148 insertions(+), 62 deletions(-) (limited to 'src/routes') diff --git a/src/routes/(app)/messages/+page.svelte b/src/routes/(app)/messages/+page.svelte index c24a20c8..f9f59bb1 100644 --- a/src/routes/(app)/messages/+page.svelte +++ b/src/routes/(app)/messages/+page.svelte @@ -7,6 +7,7 @@ } from "$lib/messaging"; import type { GroupInfo } from "$lib/messaging"; import { t } from "$lib/translations"; + import Avatar from "$comp/Avatar.svelte"; let { data } = $props(); const user = $derived(data.user); @@ -19,40 +20,41 @@ let groups: GroupInfo[] = $state([]); let loaded = $state(false); - let memberNames = $state(new Map>()); + let memberInfos = $state(new Map()); + + const otherMembers = (g: GroupInfo) => g.members.filter((pk) => pk !== user.pubkey); const groupDisplayName = (g: GroupInfo): string => { if (g.name) return g.name; - const names = memberNames.get(g.groupId); - if (!names) return "…"; - const otherNames = g.members - .filter((pk) => pk !== user.pubkey) - .map((pk) => names.get(pk) || shortPk(pk)); - return otherNames.join(", ") || "Empty group"; + const names = otherMembers(g) + .map((pk) => { const i = memberInfos.get(pk); return i ? displayName(i) : "…"; }); + return names.join(", ") || "…"; }; - const shortPk = (pk: string) => pk.slice(0, 8) + "…"; + const peerUser = (g: GroupInfo): any | null => { + if (g.members.length !== 2) return null; + const peer = otherMembers(g)[0]; + if (!peer) return null; + const info = memberInfos.get(peer); + return { pubkey: peer, username: info?.coinosUsername, picture: info?.picture }; + }; - const resolveMemberNames = async (groups: GroupInfo[]) => { - const updated = new Map(memberNames); + const resolveMembers = async (groups: GroupInfo[]) => { for (const g of groups) { - if (!updated.has(g.groupId)) updated.set(g.groupId, new Map()); - const gMap = updated.get(g.groupId)!; for (const pk of g.members) { - if (pk === user.pubkey || gMap.has(pk)) continue; + if (pk === user.pubkey || memberInfos.has(pk)) continue; resolveUser(pk).then((info) => { - gMap.set(pk, displayName(info)); - memberNames = new Map(updated); + memberInfos = new Map(memberInfos).set(pk, info); }); } } - memberNames = updated; }; const loadGroups = async () => { await fetchGroupHistory(user); - groups = await getGroups(user); - resolveMemberNames(groups); + const allGroups = await getGroups(user); + groups = allGroups.filter((g) => g.members.includes(user.pubkey)); + resolveMembers(groups); loaded = true; }; @@ -61,8 +63,9 @@ onMount(() => { loadGroups(); subscribeToMessages(user, async () => { - groups = await getGroups(user); - resolveMemberNames(groups); + const allGroups = await getGroups(user); + groups = allGroups.filter((g) => g.members.includes(user.pubkey)); + resolveMembers(groups); }).then((close) => (closeSub = close)); }); @@ -77,9 +80,15 @@ {:else} {#each groups as g} -
- {g.members.length} -
+ {#if peerUser(g)} +
+ +
+ {:else} +
+ {g.members.length} +
+ {/if}
{groupDisplayName(g)}
{#if g.lastMessage} diff --git a/src/routes/(app)/messages/[id]/+page.svelte b/src/routes/(app)/messages/[id]/+page.svelte index d7520c5d..c8ba8051 100644 --- a/src/routes/(app)/messages/[id]/+page.svelte +++ b/src/routes/(app)/messages/[id]/+page.svelte @@ -14,6 +14,7 @@ import { t } from "$lib/translations"; import { theme } from "$lib/store"; import ChatMessage from "$comp/ChatMessage.svelte"; + import Avatar from "$comp/Avatar.svelte"; const focus = (el: HTMLElement) => el.focus(); @@ -48,12 +49,22 @@ const groupDisplayName = (g: GroupInfo | undefined): string => { if (!g) return "…"; if (g.name) return g.name; - const otherMembers = g.members.filter((pk) => pk !== user.pubkey); - const names = otherMembers.map((pk) => { + // Use group members, or fall back to unique sender pubkeys from messages + let otherPks = g.members.filter((pk) => pk !== user.pubkey); + if (otherPks.length === 0) { + const senderPks = new Set(); + for (const [, dayRumours] of messageRumours) { + for (const r of dayRumours) { + if (r.pubkey && r.pubkey !== user.pubkey) senderPks.add(r.pubkey); + } + } + otherPks = Array.from(senderPks); + } + const names = otherPks.map((pk) => { const info = senderInfos.get(pk); - return info ? displayName(info) : pk.slice(0, 8) + "…"; + return info ? displayName(info) : "…"; }); - return names.join(", ") || "Group"; + return names.join(", ") || "…"; }; const dateMap = (rumours: any[]): Map => { @@ -69,6 +80,22 @@ return m; }; + type SenderRun = { pubkey: string; messages: any[] }; + + /** Group a flat list of rumours into consecutive sender runs. */ + const senderRuns = (rumours: any[]): SenderRun[] => { + const runs: SenderRun[] = []; + for (const r of rumours) { + const last = runs[runs.length - 1]; + if (last && last.pubkey === r.pubkey) { + last.messages.push(r); + } else { + runs.push({ pubkey: r.pubkey, messages: [r] }); + } + } + return runs; + }; + const updateMessages = () => { const rumours = getGroupRumours(groupId); messageRumours = dateMap(rumours); @@ -101,15 +128,22 @@ groupInfo = found; updateMessages(); + // Resolve names for group members AND rumour senders + const pubkeysToResolve = new Set(); if (groupInfo) { - for (const pk of groupInfo.members) { - if (!senderInfos.has(pk)) { - resolveUser(pk).then((info) => { - senderInfos = new Map(senderInfos).set(pk, info); - }); - } + for (const pk of groupInfo.members) pubkeysToResolve.add(pk); + } + for (const [, dayRumours] of messageRumours) { + for (const r of dayRumours) { + if (r.pubkey) pubkeysToResolve.add(r.pubkey); } } + for (const pk of pubkeysToResolve) { + if (pk === user.pubkey || senderInfos.has(pk)) continue; + resolveUser(pk).then((info) => { + senderInfos = new Map(senderInfos).set(pk, info); + }); + } await tick(); scrollChat(); @@ -118,7 +152,13 @@ const senderName = (pubkey: string): string => { if (pubkey === user.pubkey) return user.username || "You"; const info = senderInfos.get(pubkey); - return info ? displayName(info) : pubkey.slice(0, 8) + "…"; + return info ? displayName(info) : "…"; + }; + + const senderUser = (pubkey: string): any => { + if (pubkey === user.pubkey) return user; + const info = senderInfos.get(pubkey); + return { pubkey, username: info?.coinosUsername, picture: info?.picture }; }; const TIME = new Intl.DateTimeFormat(undefined, { @@ -229,21 +269,25 @@

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

{@const dayMessages = messageRumours.get(day) ?? []}
    - {#each dayMessages as rumour, i (rumour.id)} - {@const showName = i === 0 || dayMessages[i - 1]?.pubkey !== rumour.pubkey} -
  • -
    - {#if showName} -
    {senderName(rumour.pubkey)}
    - {/if} -
    - - - {TIME.format(new Date(rumour.created_at * 1000))} - -
    + {#each senderRuns(dayMessages) as run} +
  • +
    + +
    +
    + {#each run.messages as rumour, j (rumour.id)} +
    + {#if j === 0} +
    {senderName(run.pubkey)}
    + {/if} + + + {TIME.format(new Date(rumour.created_at * 1000))} + +
    + {/each}
  • {/each} @@ -314,24 +358,34 @@ color: #7f7f7f; } - .message-row { + .sender-run { display: flex; align-items: flex-end; gap: 0.5rem; - margin: 1px 0; + margin-top: 0.5rem; } - .message-content { - max-width: 85%; + .run-avatar { + flex-shrink: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + margin-bottom: -6px; + } + + .run-messages { + max-width: calc(85% - 3rem); + display: flex; + flex-direction: column; + gap: 5px; } .sender-name { font-size: 0.75rem; - font-weight: 600; - color: #7f7f7f; - margin-top: 0.5rem; + font-weight: 700; + color: var(--accent, #6366f1); margin-bottom: 1px; - margin-left: 0.75rem; } .date-header { @@ -379,20 +433,43 @@ } .message { - padding: 0.25em 0.75em; + padding: 0.35em 0.75em; width: fit-content; max-width: 100%; - border-radius: 10px; - overflow: hidden; + border-radius: 12px; + overflow: visible; word-break: break-word; + position: relative; + } + + .bubble-tail { + border-bottom-left-radius: 4px; + } + + .bubble-tail::before { + content: ""; + position: absolute; + bottom: 0; + left: -8px; + width: 10px; + height: 16px; + clip-path: path('M10 0 C10 7 2 13 0 16 L10 16 Z'); } .light-message { - background-color: #ddd; + background-color: #e4e4e4; + } + + .light-message.bubble-tail::before { + background: #e4e4e4; } .dark-message { - background-color: #222; + background-color: #2a2a2a; + } + + .dark-message.bubble-tail::before { + background: #2a2a2a; } .input-bar { -- cgit v1.2.3