feat(chat): group consecutive messages into message blocks
- Add components/RoomMessageBlock.svelte to render a block header (profile picture, display name, timestamp) and group consecutive messages from the same user. - Refactor src/app/components/RoomItem.svelte to support grouped-mode (isGrouped) and remove per-message header/spacing, keep reactions/actions intact. - Update message feed processing in: - src/routes/spaces/[relay]/[h]/+page.svelte - src/routes/spaces/[relay]/chat/+page.svelte to emit message-block items (respecting the existing 3-minute gap rule) and render blocks via RoomMessageBlock.
This commit is contained in:
@@ -37,6 +37,7 @@
|
|||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
replyTo?: (event: TrustedEvent) => void
|
replyTo?: (event: TrustedEvent) => void
|
||||||
showPubkey?: boolean
|
showPubkey?: boolean
|
||||||
|
isGrouped?: boolean
|
||||||
inert?: boolean
|
inert?: boolean
|
||||||
canEdit: (event: TrustedEvent) => boolean
|
canEdit: (event: TrustedEvent) => boolean
|
||||||
onEdit: (event: TrustedEvent) => void
|
onEdit: (event: TrustedEvent) => void
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
event,
|
event,
|
||||||
replyTo = undefined,
|
replyTo = undefined,
|
||||||
showPubkey = false,
|
showPubkey = false,
|
||||||
|
isGrouped = false,
|
||||||
inert = false,
|
inert = false,
|
||||||
canEdit,
|
canEdit,
|
||||||
onEdit,
|
onEdit,
|
||||||
@@ -74,89 +76,153 @@
|
|||||||
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
publishReaction({...template, event, relays: [url], protect: await shouldProtect})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TapTarget
|
{#if isGrouped}
|
||||||
data-event={event.id}
|
<TapTarget
|
||||||
onTap={inert ? null : onTap}
|
data-event={event.id}
|
||||||
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left hover:bg-base-100/50">
|
onTap={inert ? null : onTap}
|
||||||
<div class="flex w-full gap-3 overflow-auto">
|
class="group relative flex w-full cursor-default flex-col text-left hover:bg-base-100/50">
|
||||||
{#if showPubkey}
|
<div class="flex w-full gap-3 overflow-auto pr-1">
|
||||||
<Button onclick={openProfile} class="flex items-start">
|
<div class="min-w-0 flex-grow">
|
||||||
<ProfileCircle
|
<div>
|
||||||
pubkey={event.pubkey}
|
<RoomItemContent {url} {event} />
|
||||||
class="border border-solid border-base-content"
|
{#if thunk}
|
||||||
size={8} />
|
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||||
</Button>
|
{/if}
|
||||||
{:else}
|
|
||||||
<div class="w-8 min-w-8 max-w-8"></div>
|
|
||||||
{/if}
|
|
||||||
<div class="min-w-0 flex-grow pr-1">
|
|
||||||
{#if showPubkey}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
|
||||||
{$profileDisplay}
|
|
||||||
</Button>
|
|
||||||
<span class="text-xs opacity-50">
|
|
||||||
{#if formatTimestampAsDate(event.created_at) === today}
|
|
||||||
Today
|
|
||||||
{:else}
|
|
||||||
{formatTimestampAsDate(event.created_at)}
|
|
||||||
{/if}
|
|
||||||
at {formatTimestampAsTime(event.created_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
|
||||||
<RoomItemContent {url} {event} />
|
|
||||||
{#if thunk}
|
|
||||||
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row-2 mt-1">
|
||||||
<div class="row-2 ml-10 mt-1 pl-1">
|
<ReactionSummary
|
||||||
<ReactionSummary
|
{url}
|
||||||
{url}
|
{event}
|
||||||
{event}
|
{deleteReaction}
|
||||||
{deleteReaction}
|
{createReaction}
|
||||||
{createReaction}
|
reactionClass="tooltip-right" />
|
||||||
reactionClass="tooltip-right" />
|
{#if path && $comments.length > 0}
|
||||||
{#if path && $comments.length > 0}
|
{@const pubkeys = $comments.map(e => e.pubkey)}
|
||||||
{@const pubkeys = $comments.map(e => e.pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||||
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
{@const tooltip = `${info} commented`}
|
||||||
{@const tooltip = `${info} commented`}
|
<div data-tip={tooltip} class="tooltip tooltip-right flex">
|
||||||
<div data-tip={tooltip} class="tooltip tooltip-right flex">
|
<Link
|
||||||
<Link
|
href={path}
|
||||||
href={path}
|
class={cx("btn btn-xs gap-1 rounded-full", {
|
||||||
class={cx("btn btn-xs gap-1 rounded-full", {
|
"btn-neutral": !isOwn,
|
||||||
"btn-neutral": !isOwn,
|
"btn-primary": isOwn,
|
||||||
"btn-primary": isOwn,
|
})}>
|
||||||
})}>
|
<Icon icon={ReplyAlt} />
|
||||||
<Icon icon={ReplyAlt} />
|
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
|
||||||
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
|
</Link>
|
||||||
</Link>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if !isMobile}
|
||||||
|
<button
|
||||||
|
class="join absolute right-1 top-0 -translate-y-full border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
|
class:group-hover:opacity-100={!isMobile}>
|
||||||
|
{#if ENABLE_ZAPS}
|
||||||
|
<RoomItemZapButton {url} {event} />
|
||||||
|
{/if}
|
||||||
|
<RoomItemEmojiButton {url} {event} />
|
||||||
|
{#if replyTo}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
|
<Icon icon={Reply} size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={edit}>
|
||||||
|
<Icon icon={Pen} size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<RoomItemMenuButton {url} {event} />
|
||||||
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</TapTarget>
|
||||||
{#if !isMobile}
|
{:else}
|
||||||
<button
|
<TapTarget
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
data-event={event.id}
|
||||||
class:group-hover:opacity-100={!isMobile}>
|
onTap={inert ? null : onTap}
|
||||||
{#if ENABLE_ZAPS}
|
class="group relative flex w-full cursor-default flex-col p-2 pb-3 text-left hover:bg-base-100/50">
|
||||||
<RoomItemZapButton {url} {event} />
|
<div class="flex w-full gap-3 overflow-auto">
|
||||||
{/if}
|
{#if showPubkey}
|
||||||
<RoomItemEmojiButton {url} {event} />
|
<Button onclick={openProfile} class="flex items-start">
|
||||||
{#if replyTo}
|
<ProfileCircle
|
||||||
<Button class="btn join-item btn-xs" onclick={reply}>
|
pubkey={event.pubkey}
|
||||||
<Icon icon={Reply} size={4} />
|
class="border border-solid border-base-content"
|
||||||
|
size={8} />
|
||||||
</Button>
|
</Button>
|
||||||
|
{:else}
|
||||||
|
<div class="w-8 min-w-8 max-w-8"></div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if edit}
|
<div class="min-w-0 flex-grow pr-1">
|
||||||
<Button class="btn join-item btn-xs" onclick={edit}>
|
{#if showPubkey}
|
||||||
<Icon icon={Pen} size={4} />
|
<div class="flex items-center gap-2">
|
||||||
</Button>
|
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
|
{$profileDisplay}
|
||||||
|
</Button>
|
||||||
|
<span class="text-xs opacity-50">
|
||||||
|
{#if formatTimestampAsDate(event.created_at) === today}
|
||||||
|
Today
|
||||||
|
{:else}
|
||||||
|
{formatTimestampAsDate(event.created_at)}
|
||||||
|
{/if}
|
||||||
|
at {formatTimestampAsTime(event.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class:mt-2={showPubkey && event.kind !== MESSAGE}>
|
||||||
|
<RoomItemContent {url} {event} />
|
||||||
|
{#if thunk}
|
||||||
|
<ThunkFailure showToastOnRetry {thunk} class="mt-2 text-sm" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row-2 mt-1">
|
||||||
|
<ReactionSummary
|
||||||
|
{url}
|
||||||
|
{event}
|
||||||
|
{deleteReaction}
|
||||||
|
{createReaction}
|
||||||
|
reactionClass="tooltip-right" />
|
||||||
|
{#if path && $comments.length > 0}
|
||||||
|
{@const pubkeys = $comments.map(e => e.pubkey)}
|
||||||
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
|
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
|
||||||
|
{@const tooltip = `${info} commented`}
|
||||||
|
<div data-tip={tooltip} class="tooltip tooltip-right flex">
|
||||||
|
<Link
|
||||||
|
href={path}
|
||||||
|
class={cx("btn btn-xs gap-1 rounded-full", {
|
||||||
|
"btn-neutral": !isOwn,
|
||||||
|
"btn-primary": isOwn,
|
||||||
|
})}>
|
||||||
|
<Icon icon={ReplyAlt} />
|
||||||
|
<span>{$comments.length} comment{$comments.length === 1 ? "" : "s"}</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<RoomItemMenuButton {url} {event} />
|
</div>
|
||||||
</button>
|
{#if !isMobile}
|
||||||
{/if}
|
<button
|
||||||
</TapTarget>
|
class="join absolute right-1 top-0 -translate-y-full border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
|
class:group-hover:opacity-100={!isMobile}>
|
||||||
|
{#if ENABLE_ZAPS}
|
||||||
|
<RoomItemZapButton {url} {event} />
|
||||||
|
{/if}
|
||||||
|
<RoomItemEmojiButton {url} {event} />
|
||||||
|
{#if replyTo}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={reply}>
|
||||||
|
<Icon icon={Reply} size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if edit}
|
||||||
|
<Button class="btn join-item btn-xs" onclick={edit}>
|
||||||
|
<Icon icon={Pen} size={4} />
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<RoomItemMenuButton {url} {event} />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</TapTarget>
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {hash, formatTimestampAsTime, formatTimestampAsDate, now} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {deriveProfileDisplay} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import RoomItem from "@app/components/RoomItem.svelte"
|
||||||
|
import {colors} from "@app/core/state"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
events: TrustedEvent[]
|
||||||
|
replyTo?: (event: TrustedEvent) => void
|
||||||
|
inert?: boolean
|
||||||
|
canEdit: (event: TrustedEvent) => boolean
|
||||||
|
onEdit: (event: TrustedEvent) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, events, replyTo = undefined, inert = false, canEdit, onEdit}: Props = $props()
|
||||||
|
|
||||||
|
const firstEvent = events[0]
|
||||||
|
const today = formatTimestampAsDate(now())
|
||||||
|
const profileDisplay = deriveProfileDisplay(firstEvent.pubkey, [url])
|
||||||
|
const [_, colorValue] = colors[hash(firstEvent.pubkey) % colors.length]
|
||||||
|
|
||||||
|
const openProfile = () => pushModal(ProfileDetail, {pubkey: firstEvent.pubkey, url})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="relative flex w-full flex-col p-2 pb-3 text-left hover:bg-base-100/50">
|
||||||
|
<div class="flex w-full gap-3">
|
||||||
|
<Button onclick={openProfile} class="flex items-start">
|
||||||
|
<ProfileCircle
|
||||||
|
pubkey={firstEvent.pubkey}
|
||||||
|
class="border border-solid border-base-content"
|
||||||
|
size={8} />
|
||||||
|
</Button>
|
||||||
|
<div class="min-w-0 flex-grow">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button onclick={openProfile} class="text-sm font-bold" style="color: {colorValue}">
|
||||||
|
{$profileDisplay}
|
||||||
|
</Button>
|
||||||
|
<span class="text-xs opacity-50">
|
||||||
|
{#if formatTimestampAsDate(firstEvent.created_at) === today}
|
||||||
|
Today
|
||||||
|
{:else}
|
||||||
|
{formatTimestampAsDate(firstEvent.created_at)}
|
||||||
|
{/if}
|
||||||
|
at {formatTimestampAsTime(firstEvent.created_at)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
{#each events as event (event.id)}
|
||||||
|
<RoomItem {url} {event} {replyTo} {inert} {canEdit} {onEdit} isGrouped />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
||||||
import RoomImage from "@app/components/RoomImage.svelte"
|
import RoomImage from "@app/components/RoomImage.svelte"
|
||||||
import RoomDetail from "@app/components/RoomDetail.svelte"
|
import RoomDetail from "@app/components/RoomDetail.svelte"
|
||||||
import RoomItem from "@app/components/RoomItem.svelte"
|
import RoomMessageBlock from "@app/components/RoomMessageBlock.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
@@ -242,6 +242,7 @@
|
|||||||
let previousPubkey
|
let previousPubkey
|
||||||
let previousCreatedAt = 0
|
let previousCreatedAt = 0
|
||||||
let newMessagesSeen = false
|
let newMessagesSeen = false
|
||||||
|
let currentBlock: TrustedEvent[] = []
|
||||||
|
|
||||||
if (events) {
|
if (events) {
|
||||||
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
|
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
|
||||||
@@ -250,6 +251,17 @@
|
|||||||
const adjustedLastChecked =
|
const adjustedLastChecked =
|
||||||
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
||||||
|
|
||||||
|
const flushBlock = () => {
|
||||||
|
if (currentBlock.length > 0) {
|
||||||
|
elements.push({
|
||||||
|
type: "message-block",
|
||||||
|
id: currentBlock[0].id,
|
||||||
|
events: [...currentBlock],
|
||||||
|
})
|
||||||
|
currentBlock = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const event of $events) {
|
for (const event of $events) {
|
||||||
if (seen.has(event.id)) {
|
if (seen.has(event.id)) {
|
||||||
continue
|
continue
|
||||||
@@ -264,23 +276,26 @@
|
|||||||
event.created_at > adjustedLastChecked &&
|
event.created_at > adjustedLastChecked &&
|
||||||
event.created_at < mounted
|
event.created_at < mounted
|
||||||
) {
|
) {
|
||||||
|
flushBlock()
|
||||||
elements.push({type: "new-messages", id: "new-messages"})
|
elements.push({type: "new-messages", id: "new-messages"})
|
||||||
newMessagesSeen = true
|
newMessagesSeen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (date !== previousDate) {
|
if (date !== previousDate) {
|
||||||
elements.push({type: "date", value: date, id: date, showPubkey: false})
|
flushBlock()
|
||||||
|
elements.push({type: "date", value: date, id: date})
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push({
|
const shouldStartNewBlock =
|
||||||
id: event.id,
|
previousPubkey !== event.pubkey ||
|
||||||
type: "note",
|
event.created_at - previousCreatedAt > int(3, MINUTE) ||
|
||||||
value: event,
|
previousKind === ROOM_ADD_MEMBER
|
||||||
showPubkey:
|
|
||||||
previousPubkey !== event.pubkey ||
|
if (shouldStartNewBlock) {
|
||||||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
|
flushBlock()
|
||||||
previousKind === ROOM_ADD_MEMBER,
|
}
|
||||||
})
|
|
||||||
|
currentBlock.push(event)
|
||||||
|
|
||||||
previousDate = date
|
previousDate = date
|
||||||
previousKind = event.kind
|
previousKind = event.kind
|
||||||
@@ -288,6 +303,8 @@
|
|||||||
previousCreatedAt = event.created_at
|
previousCreatedAt = event.created_at
|
||||||
seen.add(event.id)
|
seen.add(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flushBlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.reverse()
|
elements.reverse()
|
||||||
@@ -382,32 +399,31 @@
|
|||||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
{#each elements as element (element.id)}
|
||||||
{#if type === "new-messages"}
|
{#if element.type === "new-messages"}
|
||||||
<div
|
<div
|
||||||
{id}
|
id={element.id}
|
||||||
class="flex items-center py-2 text-xs transition-colors"
|
class="flex items-center py-2 text-xs transition-colors"
|
||||||
class:opacity-0={showFixedNewMessages}>
|
class:opacity-0={showFixedNewMessages}>
|
||||||
<div class="h-px flex-grow bg-primary"></div>
|
<div class="h-px flex-grow bg-primary"></div>
|
||||||
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
||||||
<div class="h-px flex-grow bg-primary"></div>
|
<div class="h-px flex-grow bg-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
{:else if type === "date"}
|
{:else if element.type === "date"}
|
||||||
<Divider>{value}</Divider>
|
<Divider>{(element as any).value}</Divider>
|
||||||
{:else}
|
{:else if element.type === "message-block"}
|
||||||
{@const event = $state.snapshot(value as TrustedEvent)}
|
<div in:slide class="cv">
|
||||||
|
<RoomMessageBlock
|
||||||
|
{url}
|
||||||
|
events={(element as any).events}
|
||||||
|
{replyTo}
|
||||||
|
canEdit={canEditEvent}
|
||||||
|
onEdit={onEditEvent} />
|
||||||
|
</div>
|
||||||
|
{:else if (element as any).value}
|
||||||
|
{@const event = $state.snapshot((element as any).value)}
|
||||||
{#if event.kind === ROOM_ADD_MEMBER}
|
{#if event.kind === ROOM_ADD_MEMBER}
|
||||||
<RoomItemAddMember {url} {event} />
|
<RoomItemAddMember {url} {event} />
|
||||||
{:else}
|
|
||||||
<div in:slide class="cv" class:-mt-1={!showPubkey}>
|
|
||||||
<RoomItem
|
|
||||||
{url}
|
|
||||||
{event}
|
|
||||||
{replyTo}
|
|
||||||
{showPubkey}
|
|
||||||
canEdit={canEditEvent}
|
|
||||||
onEdit={onEditEvent} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import SpaceBar from "@app/components/SpaceBar.svelte"
|
import SpaceBar from "@app/components/SpaceBar.svelte"
|
||||||
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||||
import RoomItem from "@app/components/RoomItem.svelte"
|
import RoomMessageBlock from "@app/components/RoomMessageBlock.svelte"
|
||||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||||
|
|
||||||
import RoomCompose from "@app/components/RoomCompose.svelte"
|
import RoomCompose from "@app/components/RoomCompose.svelte"
|
||||||
@@ -180,6 +180,7 @@
|
|||||||
let previousPubkey
|
let previousPubkey
|
||||||
let previousCreatedAt = 0
|
let previousCreatedAt = 0
|
||||||
let newMessagesSeen = false
|
let newMessagesSeen = false
|
||||||
|
let currentBlock: TrustedEvent[] = []
|
||||||
|
|
||||||
if (events) {
|
if (events) {
|
||||||
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
|
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
|
||||||
@@ -188,6 +189,17 @@
|
|||||||
const adjustedLastChecked =
|
const adjustedLastChecked =
|
||||||
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
||||||
|
|
||||||
|
const flushBlock = () => {
|
||||||
|
if (currentBlock.length > 0) {
|
||||||
|
elements.push({
|
||||||
|
type: "message-block",
|
||||||
|
id: currentBlock[0].id,
|
||||||
|
events: [...currentBlock],
|
||||||
|
})
|
||||||
|
currentBlock = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const event of $events) {
|
for (const event of $events) {
|
||||||
if (seen.has(event.id)) {
|
if (seen.has(event.id)) {
|
||||||
continue
|
continue
|
||||||
@@ -202,23 +214,26 @@
|
|||||||
event.created_at > adjustedLastChecked &&
|
event.created_at > adjustedLastChecked &&
|
||||||
event.created_at < mounted
|
event.created_at < mounted
|
||||||
) {
|
) {
|
||||||
|
flushBlock()
|
||||||
elements.push({type: "new-messages", id: "new-messages"})
|
elements.push({type: "new-messages", id: "new-messages"})
|
||||||
newMessagesSeen = true
|
newMessagesSeen = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (date !== previousDate) {
|
if (date !== previousDate) {
|
||||||
elements.push({type: "date", value: date, id: date, showPubkey: false})
|
flushBlock()
|
||||||
|
elements.push({type: "date", value: date, id: date})
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.push({
|
const shouldStartNewBlock =
|
||||||
id: event.id,
|
previousPubkey !== event.pubkey ||
|
||||||
type: "note",
|
event.created_at - previousCreatedAt > int(3, MINUTE) ||
|
||||||
value: event,
|
previousKind === RELAY_ADD_MEMBER
|
||||||
showPubkey:
|
|
||||||
previousPubkey !== event.pubkey ||
|
if (shouldStartNewBlock) {
|
||||||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
|
flushBlock()
|
||||||
previousKind === RELAY_ADD_MEMBER,
|
}
|
||||||
})
|
|
||||||
|
currentBlock.push(event)
|
||||||
|
|
||||||
previousDate = date
|
previousDate = date
|
||||||
previousKind = event.kind
|
previousKind = event.kind
|
||||||
@@ -226,6 +241,8 @@
|
|||||||
previousCreatedAt = event.created_at
|
previousCreatedAt = event.created_at
|
||||||
seen.add(event.id)
|
seen.add(event.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
flushBlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.reverse()
|
elements.reverse()
|
||||||
@@ -295,32 +312,29 @@
|
|||||||
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#each elements as { type, id, value, showPubkey } (id)}
|
{#each elements as element (element.id)}
|
||||||
{#if type === "new-messages"}
|
{#if element.type === "new-messages"}
|
||||||
<div
|
<div
|
||||||
{id}
|
id={element.id}
|
||||||
class="flex items-center py-2 text-xs transition-colors"
|
class="flex items-center py-2 text-xs transition-colors"
|
||||||
class:opacity-0={showFixedNewMessages}>
|
class:opacity-0={showFixedNewMessages}>
|
||||||
<div class="h-px flex-grow bg-primary"></div>
|
<div class="h-px flex-grow bg-primary"></div>
|
||||||
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
<p class="rounded-full bg-primary px-2 py-1 text-primary-content">New Messages</p>
|
||||||
<div class="h-px flex-grow bg-primary"></div>
|
<div class="h-px flex-grow bg-primary"></div>
|
||||||
</div>
|
</div>
|
||||||
{:else if type === "date"}
|
{:else if element.type === "date"}
|
||||||
<Divider>{value}</Divider>
|
<Divider>{(element as any).value}</Divider>
|
||||||
{:else}
|
{:else if element.type === "message-block"}
|
||||||
{@const event = $state.snapshot(value as TrustedEvent)}
|
<RoomMessageBlock
|
||||||
|
{url}
|
||||||
|
events={(element as any).events}
|
||||||
|
{replyTo}
|
||||||
|
canEdit={canEditEvent}
|
||||||
|
onEdit={onEditEvent} />
|
||||||
|
{:else if (element as any).value}
|
||||||
|
{@const event = $state.snapshot((element as any).value)}
|
||||||
{#if event.kind === RELAY_ADD_MEMBER}
|
{#if event.kind === RELAY_ADD_MEMBER}
|
||||||
<RoomItemAddMember {url} {event} />
|
<RoomItemAddMember {url} {event} />
|
||||||
{:else}
|
|
||||||
<div class:-mt-1={!showPubkey}>
|
|
||||||
<RoomItem
|
|
||||||
{url}
|
|
||||||
{event}
|
|
||||||
{replyTo}
|
|
||||||
{showPubkey}
|
|
||||||
canEdit={canEditEvent}
|
|
||||||
onEdit={onEditEvent} />
|
|
||||||
</div>
|
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
Reference in New Issue
Block a user