diff --git a/src/app/components/RoomChat.svelte b/src/app/components/RoomChat.svelte index 4d493909..096824c0 100644 --- a/src/app/components/RoomChat.svelte +++ b/src/app/components/RoomChat.svelte @@ -6,7 +6,7 @@ import type {Readable} from "svelte/store" import {debounce} from "throttle-debounce" import cx from "classnames" - import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib" + import {now, ifLet, ago, MINUTE} from "@welshman/lib" import type {Maybe} from "@welshman/lib" import type {TrustedEvent, EventContent} from "@welshman/util" import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util" @@ -40,6 +40,7 @@ RoomType, deriveUserRoomMembershipStatus, getRoomType, + groupRoomMessages, prependParent, } from "@app/rooms" import {userSettingsValues} from "@app/settings" @@ -236,9 +237,6 @@ const getElementKey = (element: {id: string}) => element.id - // The list renders from the newest message outward, so a row deep in history isn't on the page - // until it's asked for — and asking renders it on the next flush, which the lookup has to wait - // for. Messages carry the id as a data attribute; the new-messages divider as its element id. // Where a row sits inside the scroll container, in screen terms rather than scroll terms, so // the reversed layout doesn't come into it const topOf = (target: HTMLElement) => @@ -266,6 +264,9 @@ pinned = undefined } + // The list renders from the newest message outward, so a row deep in history isn't on the page + // until it's asked for — and asking renders it on the next flush, which the lookup has to wait + // for. Messages carry the id as a data attribute; the new-messages divider as its element id. const scrollToRow = ( id: string, { @@ -421,71 +422,15 @@ isUserScrolling = false }) - const elements = $derived.by(() => { - const elements = [] - const seen = new Set() - - let previousDate - let previousKind - let previousPubkey - let previousCreatedAt = 0 - let newMessagesSeen = false - - if (events) { - const lastUserEvent = $events.findLast(e => e.pubkey === $user.pubkey) - - // Adjust the boundary to account for messages that came from a different device - const adjustedAfter = - newMessagesAfter && lastUserEvent - ? Math.max(lastUserEvent.created_at, newMessagesAfter) - : newMessagesAfter - - for (const event of $events) { - if (seen.has(event.id)) { - continue - } - - const date = formatTimestampAsDate(event.created_at) - - if ( - !newMessagesSeen && - adjustedAfter && - event.pubkey !== $user.pubkey && - event.created_at > adjustedAfter && - event.created_at < newMessagesBefore - ) { - elements.push({type: "new-messages", id: "new-messages"}) - newMessagesSeen = true - } - - if (date !== previousDate) { - elements.push({type: "date", value: date, id: date, showPubkey: false}) - } - - const showPubkey = - previousPubkey !== event.pubkey || - event.created_at - previousCreatedAt > int(3, MINUTE) || - previousKind === addMemberKind - - elements.push({ - id: event.id, - type: "note", - value: event, - showPubkey, - }) - - previousDate = date - previousKind = event.kind - previousPubkey = event.pubkey - previousCreatedAt = event.created_at - seen.add(event.id) - } - } - - elements.reverse() - - return elements - }) + const elements = $derived( + groupRoomMessages({ + events: $events, + pubkey: $user.pubkey, + addMemberKind, + unreadAfter: newMessagesAfter, + unreadBefore: newMessagesBefore, + }), + ) // Newer messages are only worth waiting for when the window stops short of the present, which // only happens after jumping into history — anything published from here on arrives through the @@ -586,6 +531,24 @@ }) +{#snippet membershipButton(label: string)} + {#if $membershipStatus === MembershipStatus.Pending} + + {:else} + + {/if} +{/snippet} +

You aren't currently a member of this room.

- {#if $membershipStatus === MembershipStatus.Pending} - - {:else} - - {/if} + {@render membershipButton("Join Room")}
{:else} @@ -672,10 +621,10 @@ getKey={getElementKey} container={element} bind:controller={virtualList}> - {#snippet row({type, id, value, showPubkey})} - {#if type === "new-messages"} + {#snippet row(item)} + {#if item.type === "new-messages"}
@@ -687,22 +636,19 @@

- {:else if type === "date"} - {value} + {:else if item.type === "date"} + {item.value} + {:else if item.value.kind === addMemberKind} + {:else} - {@const event = value as TrustedEvent} - {#if event.kind === addMemberKind} - - {:else} - - {/if} + {/if} {/snippet} @@ -743,21 +689,7 @@ {:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}

Only members are allowed to post to this room.

- {#if $membershipStatus === MembershipStatus.Pending} - - {:else} - - {/if} + {@render membershipButton("Ask to Join")}
{:else}
diff --git a/src/app/rooms.ts b/src/app/rooms.ts index a94bd1c9..9d04e0b0 100644 --- a/src/app/rooms.ts +++ b/src/app/rooms.ts @@ -1,6 +1,6 @@ import * as nip19 from "nostr-tools/nip19" import {derived} from "svelte/store" -import {sortBy, uniq} from "@welshman/lib" +import {formatTimestampAsDate, int, sortBy, uniq, MINUTE} from "@welshman/lib" import type {Maybe} from "@welshman/lib" import {MESSAGE, makeEvent, outbox, seen, toNostrURI} from "@welshman/util" import type {EventContent, TrustedEvent} from "@welshman/util" @@ -196,3 +196,82 @@ export const deriveUserRoomSearch = () => }) }, ) + +// A row of a room transcript: the messages themselves, the date dividers between them, and the +// marker for where the reader left off. +export type RoomRow = + | {type: "new-messages"; id: string} + | {type: "date"; id: string; value: string} + | {type: "note"; id: string; value: TrustedEvent; showPubkey: boolean} + +// Groups messages for display: a divider wherever the day changes, an unread marker at the point +// the reader left off, and the author shown only when it changes or enough quiet has passed. +// Returned newest first, which is the order a reversed transcript renders in. +export const groupRoomMessages = ({ + events, + pubkey, + addMemberKind, + unreadAfter, + unreadBefore, +}: { + events: TrustedEvent[] + pubkey: string + addMemberKind: number + unreadAfter: number + unreadBefore: number +}) => { + const rows: RoomRow[] = [] + const seenIds = new Set() + + // Messages the reader sent from another device aren't unread to them + const lastOwn = events.findLast(event => event.pubkey === pubkey) + const after = unreadAfter && lastOwn ? Math.max(lastOwn.created_at, unreadAfter) : unreadAfter + + let markedUnread = false + let previousDate: Maybe + let previousKind: Maybe + let previousPubkey: Maybe + let previousCreatedAt = 0 + + for (const event of events) { + if (seenIds.has(event.id)) continue + + seenIds.add(event.id) + + const date = formatTimestampAsDate(event.created_at) + + if ( + !markedUnread && + after && + event.pubkey !== pubkey && + event.created_at > after && + event.created_at < unreadBefore + ) { + rows.push({type: "new-messages", id: "new-messages"}) + markedUnread = true + } + + if (date !== previousDate) { + rows.push({type: "date", id: date, value: date}) + } + + rows.push({ + type: "note", + id: event.id, + value: event, + showPubkey: + previousPubkey !== event.pubkey || + event.created_at - previousCreatedAt > int(3, MINUTE) || + previousKind === addMemberKind, + }) + + previousDate = date + previousKind = event.kind + previousPubkey = event.pubkey + previousCreatedAt = event.created_at + } + + rows.reverse() + + return rows +}