forked from coracle/flotilla
Refactor SpaceSearch into its own component
This commit is contained in:
@@ -157,7 +157,7 @@ src/
|
|||||||
- Derive all other data inside the component from identifiers
|
- Derive all other data inside the component from identifiers
|
||||||
- Example: Don't pass `members` prop, derive it from `h` inside component
|
- Example: Don't pass `members` prop, derive it from `h` inside component
|
||||||
|
|
||||||
**Code Style:**
|
**CRITICAL Code Style Guidelines:**
|
||||||
|
|
||||||
- **No `null`** - only use `undefined`
|
- **No `null`** - only use `undefined`
|
||||||
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
|
- Svelte 5 runes (`$state`, `$derived`, `$effect`) only in UI components
|
||||||
@@ -168,6 +168,7 @@ src/
|
|||||||
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
|
- When dynamically building classes, use `cx` from `classnames` rather than embedded ternaries or svelte 4's old `class:` syntax.
|
||||||
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
||||||
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
||||||
|
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
|
|||||||
@@ -402,6 +402,10 @@ progress[value]::-webkit-progress-value {
|
|||||||
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ct {
|
||||||
|
@apply top-[calc(var(--sait)+5rem)] md:top-[calc(var(--sait)+3rem)];
|
||||||
|
}
|
||||||
|
|
||||||
/* Keyboard open state adjustments */
|
/* Keyboard open state adjustments */
|
||||||
|
|
||||||
body.keyboard-open .cb {
|
body.keyboard-open .cb {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
const openMenu = () => pushDrawer(SpaceMenu, {url})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden">
|
<Button onclick={openMenu} class="btn btn-neutral btn-sm relative md:hidden btn-square">
|
||||||
<Icon icon={MenuDots} />
|
<Icon icon={MenuDots} />
|
||||||
{#if $status.theme !== "success"}
|
{#if $status.theme !== "success"}
|
||||||
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
|
<div class="absolute right-0 top-0 -mr-1 -mt-1 h-2 w-2 rounded-full bg-{$status.theme}"></div>
|
||||||
|
|||||||
@@ -0,0 +1,168 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {tick} from "svelte"
|
||||||
|
import {createSearch} from "@welshman/app"
|
||||||
|
import {formatTimestampAsDate, groupBy, now, MINUTE, HOUR, DAY, WEEK} from "@welshman/lib"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {MESSAGE} from "@welshman/util"
|
||||||
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
||||||
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
import {fly} from "@lib/transition"
|
||||||
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import {deriveEventsForUrl} from "@app/core/state"
|
||||||
|
import {goToEvent} from "@app/util/routes"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
h?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, h}: Props = $props()
|
||||||
|
|
||||||
|
const spaceMessages = deriveEventsForUrl(
|
||||||
|
url,
|
||||||
|
h ? [{kinds: [MESSAGE], "#h": [h]}] : [{kinds: [MESSAGE]}],
|
||||||
|
)
|
||||||
|
|
||||||
|
let term = $state("")
|
||||||
|
let show = $state(false)
|
||||||
|
let input: HTMLInputElement | undefined = $state()
|
||||||
|
|
||||||
|
const open = () => {
|
||||||
|
show = true
|
||||||
|
tick().then(() => input?.focus())
|
||||||
|
}
|
||||||
|
|
||||||
|
const close = () => {
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
term = ""
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = () => {
|
||||||
|
show = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchIndex = $derived.by(() =>
|
||||||
|
createSearch($spaceMessages, {
|
||||||
|
getValue: event => event.id,
|
||||||
|
fuseOptions: {keys: ["content"]},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const results = $derived(term ? searchIndex.searchOptions(term) : [])
|
||||||
|
|
||||||
|
const eventsByAge = $derived(groupBy(e => getAgeSection(e.created_at), results))
|
||||||
|
|
||||||
|
const getAgeSection = (createdAt: number) => {
|
||||||
|
const age = now() - createdAt
|
||||||
|
|
||||||
|
if (age <= DAY) {
|
||||||
|
return "day"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age <= WEEK) {
|
||||||
|
return "week"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "older"
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAgeLabel = (createdAt: number) => {
|
||||||
|
const age = now() - createdAt
|
||||||
|
|
||||||
|
if (age < MINUTE) {
|
||||||
|
return "Just now"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age < HOUR) {
|
||||||
|
return `${Math.floor(age / MINUTE)}m ago`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (age < DAY) {
|
||||||
|
return `${Math.floor(age / HOUR)}h ago`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${Math.floor(age / DAY)}d ago`
|
||||||
|
}
|
||||||
|
|
||||||
|
const onRoomSearchResultClick = (event: TrustedEvent) => {
|
||||||
|
close()
|
||||||
|
void goToEvent(event, {keepFocus: true})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button class="btn btn-neutral btn-sm btn-square" aria-label="Search" onclick={open}>
|
||||||
|
<Icon size={4} icon={Magnifier} />
|
||||||
|
</button>
|
||||||
|
{#if show}
|
||||||
|
<button class="fixed inset-0 z-feature" aria-label="Close search" onclick={close}></button>
|
||||||
|
<div class="fixed cw top-0 right-0 z-feature p-2">
|
||||||
|
<div
|
||||||
|
class="card2 card2-sm bg-alt flex flex-col gap-2 shadow-md"
|
||||||
|
transition:fly={{y: -40, duration: 150}}>
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<strong>Search</strong>
|
||||||
|
<Button onclick={clear}>
|
||||||
|
<Icon icon={CloseCircle} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<label class="input input-sm input-bordered flex w-full items-center gap-2">
|
||||||
|
<Icon size={4} icon={Magnifier} />
|
||||||
|
<input
|
||||||
|
bind:this={input}
|
||||||
|
bind:value={term}
|
||||||
|
class="min-w-0 grow"
|
||||||
|
type="text"
|
||||||
|
placeholder={h ? "Search this room..." : "Search this space..."}
|
||||||
|
oninput={onInput} />
|
||||||
|
</label>
|
||||||
|
<div class="max-h-[65vh] overflow-y-auto">
|
||||||
|
{#if !term}
|
||||||
|
<p class="text-sm opacity-70">
|
||||||
|
{h ? "Search for messages in this room." : "Search for messages across this space."}
|
||||||
|
</p>
|
||||||
|
{:else if eventsByAge.size === 0}
|
||||||
|
<p class="text-sm opacity-70">No results found.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="col-2">
|
||||||
|
{#each eventsByAge as [key, events] (key)}
|
||||||
|
<div class="col-2">
|
||||||
|
<p class="text-xs uppercase tracking-wide opacity-60">
|
||||||
|
{#if key === "day"}
|
||||||
|
Last 24 Hours
|
||||||
|
{:else if key === "week"}
|
||||||
|
Last 7 Days
|
||||||
|
{:else}
|
||||||
|
Older
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
<div class="col-2">
|
||||||
|
{#each events as event (event.id)}
|
||||||
|
<button
|
||||||
|
class="card2 bg-alt card2-sm col-2 transition-colors hover:bg-base-200 text-left"
|
||||||
|
onclick={() => onRoomSearchResultClick(event)}>
|
||||||
|
<p class="line-clamp-2 text-sm">
|
||||||
|
{event.content.trim() || "(No text content)"}
|
||||||
|
</p>
|
||||||
|
<div class="row-2 text-xs opacity-70">
|
||||||
|
<span>{getAgeLabel(event.created_at)}</span>
|
||||||
|
<span>{formatTimestampAsDate(event.created_at)}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -131,55 +131,8 @@ export const makeFeed = ({
|
|||||||
insertEvent(event)
|
insertEvent(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
const reveal = (id: string, targetEvent?: TrustedEvent) => {
|
|
||||||
const current = get(events)
|
|
||||||
|
|
||||||
if (current.find(e => e.id === id)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const queued = get(buffer)
|
|
||||||
const index = queued.findIndex(e => e.id === id)
|
|
||||||
|
|
||||||
if (index === -1) {
|
|
||||||
const event = targetEvent || repository.getEvent(id)
|
|
||||||
|
|
||||||
if (event && matchFilters(filters, event)) {
|
|
||||||
insertEvent(event)
|
|
||||||
|
|
||||||
const next = get(events)
|
|
||||||
|
|
||||||
if (next.find(e => e.id === id)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const queuedNext = get(buffer)
|
|
||||||
const queuedIndex = queuedNext.findIndex(e => e.id === id)
|
|
||||||
|
|
||||||
if (queuedIndex > -1) {
|
|
||||||
const count = Math.max(30, queuedIndex + 1)
|
|
||||||
const chunk = queuedNext.splice(0, count)
|
|
||||||
|
|
||||||
events.update($events => [...$events, ...chunk])
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const count = Math.max(30, index + 1)
|
|
||||||
const chunk = queued.splice(0, count)
|
|
||||||
|
|
||||||
events.update($events => [...$events, ...chunk])
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
events,
|
events,
|
||||||
reveal,
|
|
||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
scroller.stop()
|
scroller.stop()
|
||||||
controller.abort()
|
controller.abort()
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
const className = cx(
|
const className = cx(
|
||||||
props.class,
|
props.class,
|
||||||
"scroll-container cw cb fixed top-[calc(var(--sait)+5rem)] md:top-[calc(var(--sait)+3rem)] z-feature overflow-y-auto overflow-x-hidden",
|
"scroll-container cw cb ct fixed z-feature overflow-y-auto overflow-x-hidden",
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, onDestroy, tick} from "svelte"
|
import {onMount, onDestroy} from "svelte"
|
||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import cx from "classnames"
|
|
||||||
import {goto} from "$app/navigation"
|
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {
|
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
||||||
pubkey,
|
import {now, int, formatTimestampAsDate, ago, MINUTE} from "@welshman/lib"
|
||||||
publishThunk,
|
|
||||||
waitForThunkError,
|
|
||||||
joinRoom,
|
|
||||||
leaveRoom,
|
|
||||||
createSearch,
|
|
||||||
} from "@welshman/app"
|
|
||||||
import {now, int, formatTimestampAsDate, ago, MINUTE, sleep} from "@welshman/lib"
|
|
||||||
import type {MakeNonOptional} from "@welshman/lib"
|
import type {MakeNonOptional} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
getTagValue,
|
|
||||||
makeEvent,
|
makeEvent,
|
||||||
makeRoomMeta,
|
makeRoomMeta,
|
||||||
MESSAGE,
|
MESSAGE,
|
||||||
ROOM_ADD_MEMBER,
|
ROOM_ADD_MEMBER,
|
||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {load} from "@welshman/net"
|
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
|
||||||
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
||||||
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
||||||
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
|
||||||
import {scrollToEvent} from "@lib/html"
|
|
||||||
import {slide, fade, fly} from "@lib/transition"
|
import {slide, fade, fly} from "@lib/transition"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
@@ -45,6 +31,7 @@
|
|||||||
import RoomDetail from "@app/components/RoomDetail.svelte"
|
import RoomDetail from "@app/components/RoomDetail.svelte"
|
||||||
import RoomItem from "@app/components/RoomItem.svelte"
|
import RoomItem from "@app/components/RoomItem.svelte"
|
||||||
import RoomName from "@app/components/RoomName.svelte"
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||||
@@ -53,8 +40,6 @@
|
|||||||
import {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands"
|
import {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands"
|
||||||
import {
|
import {
|
||||||
decodeRelay,
|
decodeRelay,
|
||||||
deriveEventsForUrl,
|
|
||||||
displayRoom,
|
|
||||||
deriveRoom,
|
deriveRoom,
|
||||||
deriveUserRoomMembershipStatus,
|
deriveUserRoomMembershipStatus,
|
||||||
MESSAGE_KINDS,
|
MESSAGE_KINDS,
|
||||||
@@ -63,10 +48,9 @@
|
|||||||
userSettingsValues,
|
userSettingsValues,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
import {makeFeed} from "@app/core/requests"
|
import {makeFeed} from "@app/core/requests"
|
||||||
import {popKey, setKey} from "@lib/implicit"
|
import {popKey} from "@lib/implicit"
|
||||||
import {checked} from "@app/util/notifications"
|
import {checked} from "@app/util/notifications"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {makeRoomPath} from "@app/util/routes"
|
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
const {h, relay} = $page.params as MakeNonOptional<typeof $page.params>
|
const {h, relay} = $page.params as MakeNonOptional<typeof $page.params>
|
||||||
@@ -76,65 +60,9 @@
|
|||||||
const room = deriveRoom(url, h)
|
const room = deriveRoom(url, h)
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
||||||
const spaceMessages = deriveEventsForUrl(url, [{kinds: [MESSAGE]}])
|
|
||||||
const pendingSearchEvent = popKey<TrustedEvent | undefined>("room_search_event")
|
|
||||||
|
|
||||||
const ageSections = [
|
|
||||||
{key: "day", label: "Last 24 Hours"},
|
|
||||||
{key: "week", label: "Last 7 Days"},
|
|
||||||
{key: "older", label: "Older"},
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type AgeSectionKey = (typeof ageSections)[number]["key"]
|
|
||||||
|
|
||||||
type RoomSearchResult = {
|
|
||||||
id: string
|
|
||||||
h: string
|
|
||||||
roomName: string
|
|
||||||
event: TrustedEvent
|
|
||||||
searchText: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type RoomSearchResultItem = RoomSearchResult & {
|
|
||||||
ageLabel: string
|
|
||||||
ageSection: AgeSectionKey
|
|
||||||
dateLabel: string
|
|
||||||
preview: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const showRoomDetail = () => pushModal(RoomDetail, {url, h})
|
const showRoomDetail = () => pushModal(RoomDetail, {url, h})
|
||||||
|
|
||||||
const showRoomSearch = () => {
|
|
||||||
showRoomSearchResults = true
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeRoomSearch = () => {
|
|
||||||
showRoomSearchResults = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearRoomSearch = () => {
|
|
||||||
roomSearchTerm = ""
|
|
||||||
showRoomSearchResults = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRoomSearchInput = () => {
|
|
||||||
showRoomSearchResults = Boolean(roomSearchTerm.trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizeSearchText = (value: string) =>
|
|
||||||
value
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
|
||||||
.replace(/\s+/g, " ")
|
|
||||||
.trim()
|
|
||||||
|
|
||||||
const matchesAllTerms = (query: string, value: string) => {
|
|
||||||
const terms = normalizeSearchText(query).split(" ").filter(Boolean)
|
|
||||||
const normalizedValue = normalizeSearchText(value)
|
|
||||||
|
|
||||||
return terms.every(term => normalizedValue.includes(term))
|
|
||||||
}
|
|
||||||
|
|
||||||
const join = async () => {
|
const join = async () => {
|
||||||
joining = true
|
joining = true
|
||||||
|
|
||||||
@@ -273,102 +201,8 @@
|
|||||||
let showScrollButton = $state(false)
|
let showScrollButton = $state(false)
|
||||||
let cleanup: () => void
|
let cleanup: () => void
|
||||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||||
let revealInFeed = (_id: string, _event?: TrustedEvent) => false
|
|
||||||
let compose: RoomCompose | undefined = $state()
|
let compose: RoomCompose | undefined = $state()
|
||||||
let eventToEdit: TrustedEvent | undefined = $state()
|
let eventToEdit: TrustedEvent | undefined = $state()
|
||||||
let roomSearchTerm = $state("")
|
|
||||||
let showRoomSearchResults = $state(false)
|
|
||||||
let jumpInFlight = $state(false)
|
|
||||||
let lastJumpId: string | undefined = $state()
|
|
||||||
|
|
||||||
const trimmedRoomSearchTerm = $derived(roomSearchTerm.trim())
|
|
||||||
|
|
||||||
const roomSearchResults = $derived.by(() => {
|
|
||||||
if (!trimmedRoomSearchTerm) {
|
|
||||||
return [] as RoomSearchResult[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const search = createSearch(
|
|
||||||
$spaceMessages.map(event => {
|
|
||||||
const eventH = getTagValue("h", event.tags) || "chat"
|
|
||||||
const roomName = eventH === "chat" ? "Space Chat" : displayRoom(url, eventH)
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: event.id,
|
|
||||||
h: eventH,
|
|
||||||
roomName,
|
|
||||||
event,
|
|
||||||
searchText: `${roomName} ${event.content}`.trim(),
|
|
||||||
} as RoomSearchResult
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
getValue: result => result.id,
|
|
||||||
fuseOptions: {keys: ["searchText", "roomName"]},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return (search.searchOptions(trimmedRoomSearchTerm) as RoomSearchResult[]).filter(result =>
|
|
||||||
matchesAllTerms(trimmedRoomSearchTerm, result.searchText),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
const spaceMessageById = $derived(
|
|
||||||
new Map($spaceMessages.map(event => [event.id, event] as const)),
|
|
||||||
)
|
|
||||||
|
|
||||||
const groupedRoomSearchResults = $derived.by(() => {
|
|
||||||
const groupedByRoom = new Map<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
h: string
|
|
||||||
roomName: string
|
|
||||||
sections: Record<AgeSectionKey, RoomSearchResultItem[]>
|
|
||||||
}
|
|
||||||
>()
|
|
||||||
|
|
||||||
for (const result of roomSearchResults) {
|
|
||||||
let roomGroup = groupedByRoom.get(result.h)
|
|
||||||
|
|
||||||
if (!roomGroup) {
|
|
||||||
roomGroup = {
|
|
||||||
h: result.h,
|
|
||||||
roomName: result.roomName,
|
|
||||||
sections: {day: [], week: [], older: []},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const preview = result.event.content.trim() || "(No text content)"
|
|
||||||
const ageSection = getAgeSection(result.event.created_at)
|
|
||||||
|
|
||||||
roomGroup.sections[ageSection].push({
|
|
||||||
...result,
|
|
||||||
ageSection,
|
|
||||||
ageLabel: getAgeLabel(result.event.created_at),
|
|
||||||
dateLabel: formatTimestampAsDate(result.event.created_at),
|
|
||||||
preview,
|
|
||||||
})
|
|
||||||
|
|
||||||
groupedByRoom.set(result.h, roomGroup)
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(groupedByRoom.values())
|
|
||||||
.sort((a, b) => {
|
|
||||||
if (a.h === h) return -1
|
|
||||||
if (b.h === h) return 1
|
|
||||||
return a.roomName.localeCompare(b.roomName)
|
|
||||||
})
|
|
||||||
.map(group => ({
|
|
||||||
...group,
|
|
||||||
visibleSections: ageSections
|
|
||||||
.map(section => ({
|
|
||||||
...section,
|
|
||||||
items: group.sections[section.key].sort(
|
|
||||||
(a, b) => b.event.created_at - a.event.created_at,
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter(section => section.items.length > 0),
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const elements = $derived.by(() => {
|
const elements = $derived.by(() => {
|
||||||
const elements = []
|
const elements = []
|
||||||
@@ -447,7 +281,6 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
events = feed.events
|
events = feed.events
|
||||||
revealInFeed = feed.reveal
|
|
||||||
cleanup = feed.cleanup
|
cleanup = feed.cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,172 +307,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAgeSection = (createdAt: number): AgeSectionKey => {
|
|
||||||
const age = now() - createdAt
|
|
||||||
|
|
||||||
if (age <= 24 * 60 * 60) {
|
|
||||||
return "day"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (age <= 7 * 24 * 60 * 60) {
|
|
||||||
return "week"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "older"
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAgeLabel = (createdAt: number) => {
|
|
||||||
const age = now() - createdAt
|
|
||||||
|
|
||||||
if (age < 60) {
|
|
||||||
return "Just now"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (age < 60 * 60) {
|
|
||||||
return `${Math.floor(age / 60)}m ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
if (age < 24 * 60 * 60) {
|
|
||||||
return `${Math.floor(age / (60 * 60))}h ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${Math.floor(age / (24 * 60 * 60))}d ago`
|
|
||||||
}
|
|
||||||
|
|
||||||
const revealMessageById = async (id: string, targetEvent?: TrustedEvent) => {
|
|
||||||
const tryScroll = async () => {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
if (await scrollToEvent(id, 0)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
await tick()
|
|
||||||
await sleep(120)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let revealed = false
|
|
||||||
let inserted = revealInFeed(id, targetEvent)
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
revealed = await tryScroll()
|
|
||||||
|
|
||||||
if (!revealed) {
|
|
||||||
await load({relays: [url], filters: [{ids: [id]}]})
|
|
||||||
inserted = revealInFeed(id, targetEvent)
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
revealed = await tryScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 18 && !revealed; i++) {
|
|
||||||
await sleep(250)
|
|
||||||
inserted = revealInFeed(id, targetEvent)
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
revealed = await tryScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
return revealed
|
|
||||||
}
|
|
||||||
|
|
||||||
const stabilizeJumpScroll = async (id: string) => {
|
|
||||||
const maybeCenter = (behavior: "auto" | "smooth") => {
|
|
||||||
const element = document.querySelector(`[data-event="${id}"]`)
|
|
||||||
|
|
||||||
if (!element) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const {top, bottom} = element.getBoundingClientRect()
|
|
||||||
const viewport = window.innerHeight
|
|
||||||
const inViewBand = top >= viewport * 0.2 && bottom <= viewport * 0.8
|
|
||||||
|
|
||||||
if (!inViewBand) {
|
|
||||||
element.scrollIntoView({behavior, block: "center"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(220)
|
|
||||||
maybeCenter("smooth")
|
|
||||||
await sleep(320)
|
|
||||||
maybeCenter("auto")
|
|
||||||
}
|
|
||||||
|
|
||||||
const openRoomSearchResult = async (eventId: string, targetH: string) => {
|
|
||||||
const targetPath = makeRoomPath(url, targetH)
|
|
||||||
const targetEvent = spaceMessageById.get(eventId)
|
|
||||||
|
|
||||||
if (targetEvent) {
|
|
||||||
setKey("room_search_event", targetEvent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($page.url.pathname === targetPath) {
|
|
||||||
await handleJump(eventId)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await goto(`${targetPath}?jump=${encodeURIComponent(eventId)}`, {
|
|
||||||
noScroll: true,
|
|
||||||
keepFocus: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearJumpParam = () => {
|
|
||||||
const next = new URL($page.url)
|
|
||||||
next.searchParams.delete("jump")
|
|
||||||
window.history.replaceState(
|
|
||||||
window.history.state,
|
|
||||||
"",
|
|
||||||
`${next.pathname}${next.search}${next.hash}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleJump = async (jumpId: string) => {
|
|
||||||
if (jumpInFlight && lastJumpId === jumpId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpInFlight = true
|
|
||||||
lastJumpId = jumpId
|
|
||||||
|
|
||||||
const targetEvent = pendingSearchEvent?.id === jumpId ? pendingSearchEvent : undefined
|
|
||||||
const revealed = await revealMessageById(jumpId, targetEvent)
|
|
||||||
|
|
||||||
if (!revealed) {
|
|
||||||
pushToast({theme: "error", message: "Could not load this older message yet."})
|
|
||||||
} else {
|
|
||||||
await stabilizeJumpScroll(jumpId)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearJumpParam()
|
|
||||||
jumpInFlight = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const onRoomSearchResultClick = (event: MouseEvent) => {
|
|
||||||
closeRoomSearch()
|
|
||||||
|
|
||||||
const eventId = (event.currentTarget as HTMLElement).dataset.eventId
|
|
||||||
const targetH = (event.currentTarget as HTMLElement).dataset.roomH || h
|
|
||||||
|
|
||||||
if (!eventId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
void openRoomSearchResult(eventId, targetH)
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const observer = new ResizeObserver(() => {
|
const observer = new ResizeObserver(() => {
|
||||||
if (dynamicPadding && chatCompose) {
|
if (dynamicPadding && chatCompose) {
|
||||||
@@ -657,18 +324,6 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const jumpId = $page.url.searchParams.get("jump")
|
|
||||||
|
|
||||||
if (!jumpId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
void handleJump(jumpId)
|
|
||||||
}, 400)
|
|
||||||
})
|
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
cleanup?.()
|
cleanup?.()
|
||||||
})
|
})
|
||||||
@@ -679,31 +334,15 @@
|
|||||||
<RoomImage {url} {h} />
|
<RoomImage {url} {h} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet title()}
|
{#snippet title()}
|
||||||
<div class="row-2">
|
<RoomName {url} {h} />
|
||||||
<RoomName {url} {h} />
|
|
||||||
<Button
|
|
||||||
class="btn btn-neutral btn-xs tooltip tooltip-bottom"
|
|
||||||
data-tip="Room information"
|
|
||||||
onclick={showRoomDetail}>
|
|
||||||
<Icon size={4} icon={InfoCircle} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<div class="row-2 w-[10.5rem] min-w-0 shrink-0 sm:w-[14rem] md:w-auto">
|
<div class="row-2 items-center">
|
||||||
<label class="input input-sm input-bordered flex min-w-0 w-full items-center gap-2 md:w-64">
|
<SpaceSearch {url} {h} />
|
||||||
<Icon size={4} icon={Magnifier} />
|
<Button class="btn btn-neutral btn-sm btn-square" onclick={showRoomDetail}>
|
||||||
<input
|
<Icon size={4} icon={InfoCircle} />
|
||||||
bind:value={roomSearchTerm}
|
</Button>
|
||||||
class="min-w-0 grow"
|
<SpaceMenuButton {url} />
|
||||||
type="text"
|
|
||||||
placeholder="Search space messages..."
|
|
||||||
onfocus={showRoomSearch}
|
|
||||||
oninput={onRoomSearchInput} />
|
|
||||||
</label>
|
|
||||||
<div class="shrink-0">
|
|
||||||
<SpaceMenuButton {url} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageBar>
|
</PageBar>
|
||||||
@@ -775,71 +414,6 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</PageContent>
|
</PageContent>
|
||||||
|
|
||||||
{#if showRoomSearchResults}
|
|
||||||
<div>
|
|
||||||
<button
|
|
||||||
class="fixed inset-0 z-feature"
|
|
||||||
aria-label="Close search results"
|
|
||||||
onclick={closeRoomSearch}></button>
|
|
||||||
<div class="cw fixed top-[calc(var(--sait)+3rem)] z-popover p-2">
|
|
||||||
<div
|
|
||||||
transition:fly={{y: -40, duration: 150}}
|
|
||||||
class="mx-auto flex w-[42rem] max-w-full flex-col overflow-hidden rounded-box border border-base-content/15 bg-base-100 shadow-xl md:ml-auto md:mr-0">
|
|
||||||
<div class="row-2 border-b border-base-100 p-3">
|
|
||||||
<strong>Search Results</strong>
|
|
||||||
<div class="grow"></div>
|
|
||||||
<Button class="btn btn-ghost btn-sm" onclick={clearRoomSearch}>Clear</Button>
|
|
||||||
<Button class="btn btn-ghost btn-sm" onclick={closeRoomSearch}>
|
|
||||||
<Icon size={4} icon={CloseCircle} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div class="max-h-[65vh] overflow-y-auto bg-base-100 p-4">
|
|
||||||
{#if !trimmedRoomSearchTerm}
|
|
||||||
<p class="text-sm opacity-70">Search for messages across all rooms in this space.</p>
|
|
||||||
{:else if groupedRoomSearchResults.length === 0}
|
|
||||||
<p class="text-sm opacity-70">No results found.</p>
|
|
||||||
{:else}
|
|
||||||
<div class="col-3">
|
|
||||||
{#each groupedRoomSearchResults as roomGroup (roomGroup.h)}
|
|
||||||
<section class="col-2">
|
|
||||||
<h4 class={cx("text-sm font-semibold", roomGroup.h === h && "text-primary")}>
|
|
||||||
{roomGroup.roomName}
|
|
||||||
</h4>
|
|
||||||
{#each roomGroup.visibleSections as section (section.key)}
|
|
||||||
<div class="col-2">
|
|
||||||
<p class="text-xs uppercase tracking-wide opacity-60">{section.label}</p>
|
|
||||||
<div class="col-2">
|
|
||||||
{#each section.items as result (result.id)}
|
|
||||||
<div class="p-1">
|
|
||||||
<button
|
|
||||||
data-event-id={result.id}
|
|
||||||
data-room-h={result.h}
|
|
||||||
class={cx(
|
|
||||||
"col-2 w-full rounded-box bg-base-300 p-4 text-left transition-colors hover:bg-base-200",
|
|
||||||
result.h === h && "border border-primary/40",
|
|
||||||
)}
|
|
||||||
onclick={onRoomSearchResultClick}>
|
|
||||||
<p class="line-clamp-2 text-sm">{result.preview}</p>
|
|
||||||
<div class="row-2 text-xs opacity-70">
|
|
||||||
<span>{result.ageLabel}</span>
|
|
||||||
<span>{result.dateLabel}</span>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</section>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
||||||
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
||||||
<!-- pass -->
|
<!-- pass -->
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount, tick} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {page} from "$app/stores"
|
import {page} from "$app/stores"
|
||||||
import type {Readable} from "svelte/store"
|
import type {Readable} from "svelte/store"
|
||||||
import {readable} from "svelte/store"
|
import {readable} from "svelte/store"
|
||||||
import {now, int, formatTimestampAsDate, MINUTE, ago, sleep} from "@welshman/lib"
|
import {now, int, formatTimestampAsDate, MINUTE, ago} from "@welshman/lib"
|
||||||
import type {TrustedEvent, EventContent} from "@welshman/util"
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
||||||
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER} from "@welshman/util"
|
import {makeEvent, MESSAGE, RELAY_ADD_MEMBER, RELAY_REMOVE_MEMBER} from "@welshman/util"
|
||||||
import {pubkey, publishThunk} from "@welshman/app"
|
import {pubkey, publishThunk} from "@welshman/app"
|
||||||
import {load} from "@welshman/net"
|
|
||||||
import {scrollToEvent} from "@lib/html"
|
|
||||||
import {fade, fly} from "@lib/transition"
|
import {fade, fly} from "@lib/transition"
|
||||||
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
import ChatRound from "@assets/icons/chat-round.svg?dataurl"
|
||||||
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
||||||
@@ -19,6 +17,7 @@
|
|||||||
import PageContent from "@lib/components/PageContent.svelte"
|
import PageContent from "@lib/components/PageContent.svelte"
|
||||||
import Divider from "@lib/components/Divider.svelte"
|
import Divider from "@lib/components/Divider.svelte"
|
||||||
import ThunkToast from "@app/components/ThunkToast.svelte"
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
||||||
|
import SpaceSearch from "@app/components/SpaceSearch.svelte"
|
||||||
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
||||||
import RoomItem from "@app/components/RoomItem.svelte"
|
import RoomItem from "@app/components/RoomItem.svelte"
|
||||||
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
||||||
@@ -37,7 +36,6 @@
|
|||||||
const lastChecked = $checked[$page.url.pathname]
|
const lastChecked = $checked[$page.url.pathname]
|
||||||
const url = decodeRelay($page.params.relay!)
|
const url = decodeRelay($page.params.relay!)
|
||||||
const shouldProtect = canEnforceNip70(url)
|
const shouldProtect = canEnforceNip70(url)
|
||||||
const pendingSearchEvent = popKey<TrustedEvent | undefined>("room_search_event")
|
|
||||||
|
|
||||||
const replyTo = (event: TrustedEvent) => {
|
const replyTo = (event: TrustedEvent) => {
|
||||||
parent = event
|
parent = event
|
||||||
@@ -139,11 +137,8 @@
|
|||||||
let showScrollButton = $state(false)
|
let showScrollButton = $state(false)
|
||||||
let cleanup: () => void
|
let cleanup: () => void
|
||||||
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
||||||
let revealInFeed = (_id: string, _event?: TrustedEvent) => false
|
|
||||||
let compose: RoomCompose | undefined = $state()
|
let compose: RoomCompose | undefined = $state()
|
||||||
let eventToEdit: TrustedEvent | undefined = $state()
|
let eventToEdit: TrustedEvent | undefined = $state()
|
||||||
let jumpInFlight = $state(false)
|
|
||||||
let lastJumpId: string | undefined = $state()
|
|
||||||
|
|
||||||
const elements = $derived.by(() => {
|
const elements = $derived.by(() => {
|
||||||
const elements = []
|
const elements = []
|
||||||
@@ -232,108 +227,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const revealMessageById = async (id: string, targetEvent?: TrustedEvent) => {
|
|
||||||
const tryScroll = async () => {
|
|
||||||
for (let i = 0; i < 4; i++) {
|
|
||||||
if (await scrollToEvent(id, 0)) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
await tick()
|
|
||||||
await sleep(120)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
let revealed = false
|
|
||||||
let inserted = revealInFeed(id, targetEvent)
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
revealed = await tryScroll()
|
|
||||||
|
|
||||||
if (!revealed) {
|
|
||||||
await load({relays: [url], filters: [{ids: [id]}]})
|
|
||||||
inserted = revealInFeed(id, targetEvent)
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
revealed = await tryScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < 18 && !revealed; i++) {
|
|
||||||
await sleep(250)
|
|
||||||
inserted = revealInFeed(id, targetEvent)
|
|
||||||
|
|
||||||
if (inserted) {
|
|
||||||
await tick()
|
|
||||||
}
|
|
||||||
|
|
||||||
revealed = await tryScroll()
|
|
||||||
}
|
|
||||||
|
|
||||||
return revealed
|
|
||||||
}
|
|
||||||
|
|
||||||
const stabilizeJumpScroll = async (id: string) => {
|
|
||||||
const maybeCenter = (behavior: "auto" | "smooth") => {
|
|
||||||
const element = document.querySelector(`[data-event="${id}"]`)
|
|
||||||
|
|
||||||
if (!element) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const {top, bottom} = element.getBoundingClientRect()
|
|
||||||
const viewport = window.innerHeight
|
|
||||||
const inViewBand = top >= viewport * 0.2 && bottom <= viewport * 0.8
|
|
||||||
|
|
||||||
if (!inViewBand) {
|
|
||||||
element.scrollIntoView({behavior, block: "center"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await sleep(220)
|
|
||||||
maybeCenter("smooth")
|
|
||||||
await sleep(320)
|
|
||||||
maybeCenter("auto")
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearJumpParam = () => {
|
|
||||||
const next = new URL($page.url)
|
|
||||||
next.searchParams.delete("jump")
|
|
||||||
window.history.replaceState(
|
|
||||||
window.history.state,
|
|
||||||
"",
|
|
||||||
`${next.pathname}${next.search}${next.hash}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleJump = async (jumpId: string) => {
|
|
||||||
if (jumpInFlight && lastJumpId === jumpId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jumpInFlight = true
|
|
||||||
lastJumpId = jumpId
|
|
||||||
|
|
||||||
const targetEvent = pendingSearchEvent?.id === jumpId ? pendingSearchEvent : undefined
|
|
||||||
const revealed = await revealMessageById(jumpId, targetEvent)
|
|
||||||
|
|
||||||
if (!revealed) {
|
|
||||||
pushToast({theme: "error", message: "Could not load this older message yet."})
|
|
||||||
} else {
|
|
||||||
await stabilizeJumpScroll(jumpId)
|
|
||||||
}
|
|
||||||
|
|
||||||
clearJumpParam()
|
|
||||||
jumpInFlight = false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
|
|
||||||
@@ -356,7 +249,6 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
events = feed.events
|
events = feed.events
|
||||||
revealInFeed = feed.reveal
|
|
||||||
cleanup = feed.cleanup
|
cleanup = feed.cleanup
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@@ -371,18 +263,6 @@
|
|||||||
}, 800)
|
}, 800)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
const jumpId = $page.url.searchParams.get("jump")
|
|
||||||
|
|
||||||
if (!jumpId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
void handleJump(jumpId)
|
|
||||||
}, 400)
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<PageBar>
|
<PageBar>
|
||||||
@@ -395,7 +275,10 @@
|
|||||||
<strong>Chat</strong>
|
<strong>Chat</strong>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet action()}
|
{#snippet action()}
|
||||||
<SpaceMenuButton {url} />
|
<div class="row-2 items-center">
|
||||||
|
<SpaceSearch {url} />
|
||||||
|
<SpaceMenuButton {url} />
|
||||||
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</PageBar>
|
</PageBar>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user