refactor rooms a tad
This commit is contained in:
parent
fe3afa248d
commit
2fb2b59a6c
2 changed files with 129 additions and 118 deletions
|
|
@ -6,7 +6,7 @@
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {debounce} from "throttle-debounce"
|
import {debounce} from "throttle-debounce"
|
||||||
import cx from "classnames"
|
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 {Maybe} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, ROOM_ADD_MEMBER} from "@welshman/util"
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
RoomType,
|
RoomType,
|
||||||
deriveUserRoomMembershipStatus,
|
deriveUserRoomMembershipStatus,
|
||||||
getRoomType,
|
getRoomType,
|
||||||
|
groupRoomMessages,
|
||||||
prependParent,
|
prependParent,
|
||||||
} from "@app/rooms"
|
} from "@app/rooms"
|
||||||
import {userSettingsValues} from "@app/settings"
|
import {userSettingsValues} from "@app/settings"
|
||||||
|
|
@ -236,9 +237,6 @@
|
||||||
|
|
||||||
const getElementKey = (element: {id: string}) => element.id
|
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
|
// Where a row sits inside the scroll container, in screen terms rather than scroll terms, so
|
||||||
// the reversed layout doesn't come into it
|
// the reversed layout doesn't come into it
|
||||||
const topOf = (target: HTMLElement) =>
|
const topOf = (target: HTMLElement) =>
|
||||||
|
|
@ -266,6 +264,9 @@
|
||||||
pinned = undefined
|
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 = (
|
const scrollToRow = (
|
||||||
id: string,
|
id: string,
|
||||||
{
|
{
|
||||||
|
|
@ -421,71 +422,15 @@
|
||||||
isUserScrolling = false
|
isUserScrolling = false
|
||||||
})
|
})
|
||||||
|
|
||||||
const elements = $derived.by(() => {
|
const elements = $derived(
|
||||||
const elements = []
|
groupRoomMessages({
|
||||||
const seen = new Set()
|
events: $events,
|
||||||
|
pubkey: $user.pubkey,
|
||||||
let previousDate
|
addMemberKind,
|
||||||
let previousKind
|
unreadAfter: newMessagesAfter,
|
||||||
let previousPubkey
|
unreadBefore: newMessagesBefore,
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
// Newer messages are only worth waiting for when the window stops short of the present, which
|
// 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
|
// only happens after jumping into history — anything published from here on arrives through the
|
||||||
|
|
@ -586,6 +531,24 @@
|
||||||
})
|
})
|
||||||
</script>
|
</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
|
<div
|
||||||
class={cx(
|
class={cx(
|
||||||
"room flex min-h-0 min-w-0 flex-1 flex-col",
|
"room flex min-h-0 min-w-0 flex-1 flex-col",
|
||||||
|
|
@ -644,21 +607,7 @@
|
||||||
<div class="py-20">
|
<div class="py-20">
|
||||||
<div class="card flex flex-col gap-8 m-auto max-w-md items-center text-center">
|
<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>
|
<p class="opacity-75">You aren't currently a member of this room.</p>
|
||||||
{#if $membershipStatus === MembershipStatus.Pending}
|
{@render membershipButton("Join Room")}
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -672,10 +621,10 @@
|
||||||
getKey={getElementKey}
|
getKey={getElementKey}
|
||||||
container={element}
|
container={element}
|
||||||
bind:controller={virtualList}>
|
bind:controller={virtualList}>
|
||||||
{#snippet row({type, id, value, showPubkey})}
|
{#snippet row(item)}
|
||||||
{#if type === "new-messages"}
|
{#if item.type === "new-messages"}
|
||||||
<div
|
<div
|
||||||
{id}
|
id={item.id}
|
||||||
class={cx("flex items-center py-2 text-xs transition-colors", {
|
class={cx("flex items-center py-2 text-xs transition-colors", {
|
||||||
"opacity-0": showFixedNewMessages,
|
"opacity-0": showFixedNewMessages,
|
||||||
})}>
|
})}>
|
||||||
|
|
@ -687,22 +636,19 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="h-px grow bg-primary text-primary-content"></div>
|
<div class="h-px grow bg-primary text-primary-content"></div>
|
||||||
</div>
|
</div>
|
||||||
{:else if type === "date"}
|
{:else if item.type === "date"}
|
||||||
<Divider>{value}</Divider>
|
<Divider>{item.value}</Divider>
|
||||||
|
{:else if item.value.kind === addMemberKind}
|
||||||
|
<RoomItemAddMember {url} event={item.value} />
|
||||||
{:else}
|
{:else}
|
||||||
{@const event = value as TrustedEvent}
|
<RoomItem
|
||||||
{#if event.kind === addMemberKind}
|
{url}
|
||||||
<RoomItemAddMember {url} {event} />
|
{replyTo}
|
||||||
{:else}
|
{context}
|
||||||
<RoomItem
|
event={item.value}
|
||||||
{url}
|
showPubkey={item.showPubkey}
|
||||||
{event}
|
canEdit={canEditEvent}
|
||||||
{replyTo}
|
onEdit={onEditEvent} />
|
||||||
{showPubkey}
|
|
||||||
{context}
|
|
||||||
canEdit={canEditEvent}
|
|
||||||
onEdit={onEditEvent} />
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</VirtualList>
|
</VirtualList>
|
||||||
|
|
@ -743,21 +689,7 @@
|
||||||
{:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}
|
{:else if $room?.meta?.isRestricted() && $membershipStatus !== MembershipStatus.Granted}
|
||||||
<div class="card m-4 flex flex-row items-center justify-between px-4 py-3">
|
<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>
|
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||||
{#if $membershipStatus === MembershipStatus.Pending}
|
{@render membershipButton("Ask to Join")}
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import * as nip19 from "nostr-tools/nip19"
|
import * as nip19 from "nostr-tools/nip19"
|
||||||
import {derived} from "svelte/store"
|
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 type {Maybe} from "@welshman/lib"
|
||||||
import {MESSAGE, makeEvent, outbox, seen, toNostrURI} from "@welshman/util"
|
import {MESSAGE, makeEvent, outbox, seen, toNostrURI} from "@welshman/util"
|
||||||
import type {EventContent, TrustedEvent} 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
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue