refactor rooms a tad

This commit is contained in:
Jon Staab 2026-08-31 08:57:39 -07:00
parent fe3afa248d
commit 2fb2b59a6c
2 changed files with 129 additions and 118 deletions

View file

@ -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 @@
})
</script>
{#snippet membershipButton(label: string)}
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
<Icon icon={ClockCircle} />
Access Pending
</Button>
{:else}
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
{#if joining}
<Spinner size="sm" />
{:else}
<Icon icon={Login2} />
{/if}
{label}
</Button>
{/if}
{/snippet}
<div
class={cx(
"room flex min-h-0 min-w-0 flex-1 flex-col",
@ -644,21 +607,7 @@
<div class="py-20">
<div class="card flex flex-col gap-8 m-auto max-w-md items-center text-center">
<p class="opacity-75">You aren't currently a member of this room.</p>
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
<Icon icon={ClockCircle} />
Access Pending
</Button>
{:else}
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
{#if joining}
<Spinner size="sm" />
{:else}
<Icon icon={Login2} />
{/if}
Join Room
</Button>
{/if}
{@render membershipButton("Join Room")}
</div>
</div>
{: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"}
<div
{id}
id={item.id}
class={cx("flex items-center py-2 text-xs transition-colors", {
"opacity-0": showFixedNewMessages,
})}>
@ -687,22 +636,19 @@
</p>
<div class="h-px grow bg-primary text-primary-content"></div>
</div>
{:else if type === "date"}
<Divider>{value}</Divider>
{:else if item.type === "date"}
<Divider>{item.value}</Divider>
{:else if item.value.kind === addMemberKind}
<RoomItemAddMember {url} event={item.value} />
{:else}
{@const event = value as TrustedEvent}
{#if event.kind === addMemberKind}
<RoomItemAddMember {url} {event} />
{:else}
<RoomItem
{url}
{event}
{replyTo}
{showPubkey}
{context}
canEdit={canEditEvent}
onEdit={onEditEvent} />
{/if}
<RoomItem
{url}
{replyTo}
{context}
event={item.value}
showPubkey={item.showPubkey}
canEdit={canEditEvent}
onEdit={onEditEvent} />
{/if}
{/snippet}
</VirtualList>
@ -743,21 +689,7 @@
{:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}
<div class="card m-4 flex flex-row items-center justify-between px-4 py-3">
<p class="opacity-75">Only members are allowed to post to this room.</p>
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="button button-neutral button-sm" disabled={leaving} onclick={leave}>
<Icon icon={ClockCircle} />
Access Pending
</Button>
{:else}
<Button class="button button-neutral button-sm" disabled={joining} onclick={join}>
{#if joining}
<Spinner size="sm" />
{:else}
<Icon icon={Login2} />
{/if}
Ask to Join
</Button>
{/if}
{@render membershipButton("Ask to Join")}
</div>
{:else}
<div>

View file

@ -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<string>()
// 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<string>
let previousKind: Maybe<number>
let previousPubkey: Maybe<string>
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
}