2024-08-14 00:58:26 +00:00
|
|
|
<script lang="ts">
|
2026-02-15 02:27:36 +00:00
|
|
|
import {onMount, onDestroy, tick} from "svelte"
|
2026-02-02 17:51:36 +00:00
|
|
|
import {readable} from "svelte/store"
|
2026-02-15 02:27:36 +00:00
|
|
|
import cx from "classnames"
|
|
|
|
|
import {goto} from "$app/navigation"
|
2024-08-16 17:50:38 +00:00
|
|
|
import {page} from "$app/stores"
|
2025-01-28 16:13:20 +00:00
|
|
|
import type {Readable} from "svelte/store"
|
2026-02-15 02:27:36 +00:00
|
|
|
import {
|
|
|
|
|
pubkey,
|
|
|
|
|
publishThunk,
|
|
|
|
|
waitForThunkError,
|
|
|
|
|
joinRoom,
|
|
|
|
|
leaveRoom,
|
|
|
|
|
createSearch,
|
|
|
|
|
} from "@welshman/app"
|
|
|
|
|
import {now, int, formatTimestampAsDate, ago, MINUTE, sleep} from "@welshman/lib"
|
2025-10-13 20:35:02 +00:00
|
|
|
import type {MakeNonOptional} from "@welshman/lib"
|
2024-10-01 18:47:21 +00:00
|
|
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
2025-10-15 16:04:17 +00:00
|
|
|
import {
|
2026-02-15 02:27:36 +00:00
|
|
|
getTagValue,
|
2025-10-15 16:04:17 +00:00
|
|
|
makeEvent,
|
|
|
|
|
makeRoomMeta,
|
|
|
|
|
MESSAGE,
|
|
|
|
|
ROOM_ADD_MEMBER,
|
|
|
|
|
ROOM_REMOVE_MEMBER,
|
|
|
|
|
} from "@welshman/util"
|
2026-02-15 02:27:36 +00:00
|
|
|
import {load} from "@welshman/net"
|
|
|
|
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
|
|
|
|
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
|
2025-09-10 22:26:39 +00:00
|
|
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
2026-02-15 02:27:36 +00:00
|
|
|
import InfoCircle from "@assets/icons/info-circle.svg?dataurl"
|
2025-09-12 16:11:53 +00:00
|
|
|
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
2026-02-15 02:27:36 +00:00
|
|
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
|
|
|
|
import {scrollToEvent} from "@lib/html"
|
|
|
|
|
import {slide, fade, fly} from "@lib/transition"
|
2024-09-11 19:19:45 +00:00
|
|
|
import Button from "@lib/components/Button.svelte"
|
2026-02-15 02:27:36 +00:00
|
|
|
import Divider from "@lib/components/Divider.svelte"
|
|
|
|
|
import Icon from "@lib/components/Icon.svelte"
|
2024-10-09 23:11:23 +00:00
|
|
|
import PageBar from "@lib/components/PageBar.svelte"
|
2025-03-19 17:30:36 +00:00
|
|
|
import PageContent from "@lib/components/PageContent.svelte"
|
2026-02-15 02:27:36 +00:00
|
|
|
import Spinner from "@lib/components/Spinner.svelte"
|
|
|
|
|
import RoomCompose from "@app/components/RoomCompose.svelte"
|
|
|
|
|
import RoomComposeParent from "@app/components/RoomComposeParent.svelte"
|
2025-11-06 00:59:17 +00:00
|
|
|
import RoomImage from "@app/components/RoomImage.svelte"
|
2025-11-10 22:59:15 +00:00
|
|
|
import RoomDetail from "@app/components/RoomDetail.svelte"
|
2025-10-30 22:36:14 +00:00
|
|
|
import RoomItem from "@app/components/RoomItem.svelte"
|
2026-02-15 02:27:36 +00:00
|
|
|
import RoomName from "@app/components/RoomName.svelte"
|
|
|
|
|
import SpaceMenuButton from "@app/components/SpaceMenuButton.svelte"
|
|
|
|
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
2025-10-30 22:36:14 +00:00
|
|
|
import RoomItemAddMember from "@src/app/components/RoomItemAddMember.svelte"
|
|
|
|
|
import RoomComposeEdit from "@src/app/components/RoomComposeEdit.svelte"
|
2026-02-15 02:27:36 +00:00
|
|
|
import RoomItemRemoveMember from "@src/app/components/RoomItemRemoveMember.svelte"
|
|
|
|
|
import {canEnforceNip70, prependParent, publishDelete} from "@app/core/commands"
|
2024-10-08 18:39:16 +00:00
|
|
|
import {
|
2024-10-21 17:30:12 +00:00
|
|
|
decodeRelay,
|
2026-02-15 02:27:36 +00:00
|
|
|
deriveEventsForUrl,
|
|
|
|
|
displayRoom,
|
2025-10-30 22:36:14 +00:00
|
|
|
deriveRoom,
|
2026-02-15 02:27:36 +00:00
|
|
|
deriveUserRoomMembershipStatus,
|
|
|
|
|
MESSAGE_KINDS,
|
2025-05-28 23:46:41 +00:00
|
|
|
MembershipStatus,
|
2025-10-06 18:23:19 +00:00
|
|
|
PROTECTED,
|
2026-01-30 16:41:58 +00:00
|
|
|
userSettingsValues,
|
2025-08-21 22:01:31 +00:00
|
|
|
} from "@app/core/state"
|
|
|
|
|
import {makeFeed} from "@app/core/requests"
|
2026-02-15 02:27:36 +00:00
|
|
|
import {popKey, setKey} from "@lib/implicit"
|
|
|
|
|
import {checked} from "@app/util/notifications"
|
2025-10-24 20:36:59 +00:00
|
|
|
import {pushModal} from "@app/util/modal"
|
2026-02-15 02:27:36 +00:00
|
|
|
import {makeRoomPath} from "@app/util/routes"
|
|
|
|
|
import {pushToast} from "@app/util/toast"
|
2024-08-15 19:08:07 +00:00
|
|
|
|
2025-10-30 22:33:34 +00:00
|
|
|
const {h, relay} = $page.params as MakeNonOptional<typeof $page.params>
|
2025-05-09 19:26:05 +00:00
|
|
|
const mounted = now()
|
2025-01-28 22:19:26 +00:00
|
|
|
const lastChecked = $checked[$page.url.pathname]
|
2025-10-13 20:35:02 +00:00
|
|
|
const url = decodeRelay(relay)
|
2025-10-30 22:36:14 +00:00
|
|
|
const room = deriveRoom(url, h)
|
2025-08-19 21:03:04 +00:00
|
|
|
const shouldProtect = canEnforceNip70(url)
|
2025-10-30 22:33:34 +00:00
|
|
|
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
|
2026-02-15 02:27:36 +00:00
|
|
|
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
|
|
|
|
|
}
|
2025-01-02 23:08:16 +00:00
|
|
|
|
2025-11-10 22:59:15 +00:00
|
|
|
const showRoomDetail = () => pushModal(RoomDetail, {url, h})
|
|
|
|
|
|
2026-02-15 02:27:36 +00:00
|
|
|
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))
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
const join = async () => {
|
|
|
|
|
joining = true
|
2025-02-13 23:34:14 +00:00
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
try {
|
2025-10-30 22:33:34 +00:00
|
|
|
const message = await waitForThunkError(joinRoom(url, makeRoomMeta({h})))
|
2024-12-10 17:44:04 +00:00
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
if (message && !message.startsWith("duplicate:")) {
|
|
|
|
|
return pushToast({theme: "error", message})
|
|
|
|
|
}
|
2025-11-11 21:39:32 +00:00
|
|
|
|
|
|
|
|
// Restart the feed now that we're a member
|
|
|
|
|
start()
|
2025-05-28 23:46:41 +00:00
|
|
|
} finally {
|
|
|
|
|
joining = false
|
|
|
|
|
}
|
2024-12-10 17:44:04 +00:00
|
|
|
}
|
|
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
const leave = async () => {
|
|
|
|
|
leaving = true
|
|
|
|
|
try {
|
2025-10-30 22:33:34 +00:00
|
|
|
const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({h})))
|
2025-05-28 23:46:41 +00:00
|
|
|
|
|
|
|
|
if (message && !message.startsWith("duplicate:")) {
|
|
|
|
|
pushToast({theme: "error", message})
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
leaving = false
|
|
|
|
|
}
|
2024-12-10 17:44:04 +00:00
|
|
|
}
|
|
|
|
|
|
2024-11-19 21:24:18 +00:00
|
|
|
const replyTo = (event: TrustedEvent) => {
|
2025-01-28 16:13:20 +00:00
|
|
|
parent = event
|
2025-02-04 00:37:14 +00:00
|
|
|
compose?.focus()
|
2025-01-28 16:13:20 +00:00
|
|
|
}
|
2024-11-19 21:24:18 +00:00
|
|
|
|
2025-01-28 16:13:20 +00:00
|
|
|
const clearParent = () => {
|
|
|
|
|
parent = undefined
|
2024-11-19 21:24:18 +00:00
|
|
|
}
|
|
|
|
|
|
2025-02-06 19:33:01 +00:00
|
|
|
const clearShare = () => {
|
|
|
|
|
share = undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 14:00:23 +00:00
|
|
|
const clearEventToEdit = () => {
|
|
|
|
|
eventToEdit = undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-30 23:30:03 +00:00
|
|
|
const onSubmit = async ({content, tags}: EventContent) => {
|
2026-02-16 16:16:03 +00:00
|
|
|
try {
|
|
|
|
|
tags.push(["h", h])
|
2025-01-28 16:13:20 +00:00
|
|
|
|
2026-02-16 16:16:03 +00:00
|
|
|
if (await shouldProtect) {
|
|
|
|
|
tags.push(PROTECTED)
|
|
|
|
|
}
|
2025-10-15 14:00:23 +00:00
|
|
|
|
2026-02-16 16:16:03 +00:00
|
|
|
let template: EventContent & {created_at?: number} = {content, tags}
|
|
|
|
|
|
|
|
|
|
if (eventToEdit) {
|
|
|
|
|
// Don't do anything if message hasn't changed
|
|
|
|
|
if (eventToEdit.content === content) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Delete previous message, to be republished with same timestamp
|
|
|
|
|
template.created_at = eventToEdit.created_at
|
|
|
|
|
publishDelete({
|
|
|
|
|
relays: [url],
|
|
|
|
|
event: $state.snapshot(eventToEdit),
|
|
|
|
|
protect: await shouldProtect,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-02-06 19:33:01 +00:00
|
|
|
|
2026-02-16 16:16:03 +00:00
|
|
|
if (share) {
|
|
|
|
|
template = prependParent(share, template, url)
|
|
|
|
|
}
|
2025-02-06 19:33:01 +00:00
|
|
|
|
2026-02-16 16:16:03 +00:00
|
|
|
if (parent) {
|
|
|
|
|
template = prependParent(parent, template, url)
|
|
|
|
|
}
|
2025-02-06 19:33:01 +00:00
|
|
|
|
2026-02-16 16:16:03 +00:00
|
|
|
const thunk = publishThunk({
|
|
|
|
|
relays: [url],
|
|
|
|
|
event: makeEvent(MESSAGE, template),
|
|
|
|
|
delay: $userSettingsValues.send_delay,
|
2025-10-02 18:28:11 +00:00
|
|
|
})
|
2025-08-19 21:03:04 +00:00
|
|
|
|
2026-02-16 16:16:03 +00:00
|
|
|
if ($userSettingsValues.send_delay) {
|
|
|
|
|
pushToast({
|
|
|
|
|
timeout: 30_000,
|
|
|
|
|
children: {
|
|
|
|
|
component: ThunkToast,
|
|
|
|
|
props: {thunk},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
clearParent()
|
|
|
|
|
clearShare()
|
|
|
|
|
clearEventToEdit()
|
|
|
|
|
}
|
2025-01-28 16:13:20 +00:00
|
|
|
}
|
2024-10-01 18:47:21 +00:00
|
|
|
|
2025-01-28 22:19:26 +00:00
|
|
|
const onScroll = () => {
|
|
|
|
|
showScrollButton = Math.abs(element?.scrollTop || 0) > 1500
|
|
|
|
|
|
2025-12-17 20:07:30 +00:00
|
|
|
const newMessages = document.getElementById("new-messages")
|
|
|
|
|
|
2025-01-28 22:19:26 +00:00
|
|
|
if (!newMessages || newMessagesSeen) {
|
|
|
|
|
showFixedNewMessages = false
|
|
|
|
|
} else {
|
|
|
|
|
const {y} = newMessages.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
if (y > 300) {
|
|
|
|
|
newMessagesSeen = true
|
|
|
|
|
} else {
|
|
|
|
|
showFixedNewMessages = y < 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scrollToNewMessages = () =>
|
2025-12-17 20:07:30 +00:00
|
|
|
document.getElementById("new-messages")?.scrollIntoView({behavior: "smooth", block: "center"})
|
2025-01-28 22:19:26 +00:00
|
|
|
|
2025-02-04 00:37:14 +00:00
|
|
|
const scrollToBottom = () => element?.scrollTo({top: 0, behavior: "smooth"})
|
2025-01-28 16:13:20 +00:00
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
let joining = $state(false)
|
|
|
|
|
let leaving = $state(false)
|
2025-02-13 23:34:14 +00:00
|
|
|
let loadingEvents = $state(true)
|
2025-02-06 19:33:01 +00:00
|
|
|
let share = $state(popKey<TrustedEvent | undefined>("share"))
|
2025-02-04 00:37:14 +00:00
|
|
|
let parent: TrustedEvent | undefined = $state()
|
|
|
|
|
let element: HTMLElement | undefined = $state()
|
2025-03-04 20:53:38 +00:00
|
|
|
let chatCompose: HTMLElement | undefined = $state()
|
2025-03-04 01:46:15 +00:00
|
|
|
let dynamicPadding: HTMLElement | undefined = $state()
|
2025-01-28 22:19:26 +00:00
|
|
|
let newMessagesSeen = false
|
2025-02-04 00:37:14 +00:00
|
|
|
let showFixedNewMessages = $state(false)
|
|
|
|
|
let showScrollButton = $state(false)
|
2025-01-28 16:13:20 +00:00
|
|
|
let cleanup: () => void
|
2025-02-04 00:37:14 +00:00
|
|
|
let events: Readable<TrustedEvent[]> = $state(readable([]))
|
2026-02-15 02:27:36 +00:00
|
|
|
let revealInFeed = (_id: string, _event?: TrustedEvent) => false
|
2025-10-30 22:36:14 +00:00
|
|
|
let compose: RoomCompose | undefined = $state()
|
2025-10-15 14:00:23 +00:00
|
|
|
let eventToEdit: TrustedEvent | undefined = $state()
|
2026-02-15 02:27:36 +00:00
|
|
|
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),
|
|
|
|
|
}))
|
|
|
|
|
})
|
2024-08-15 21:30:55 +00:00
|
|
|
|
2025-02-04 00:37:14 +00:00
|
|
|
const elements = $derived.by(() => {
|
|
|
|
|
const elements = []
|
2025-01-28 16:13:20 +00:00
|
|
|
const seen = new Set()
|
2024-08-15 21:30:55 +00:00
|
|
|
|
2024-12-09 22:03:59 +00:00
|
|
|
let previousDate
|
2025-11-14 00:39:44 +00:00
|
|
|
let previousKind
|
2024-12-09 22:03:59 +00:00
|
|
|
let previousPubkey
|
2025-12-04 22:27:43 +00:00
|
|
|
let previousCreatedAt = 0
|
2025-01-28 22:19:26 +00:00
|
|
|
let newMessagesSeen = false
|
2024-08-15 21:30:55 +00:00
|
|
|
|
2025-01-28 16:13:20 +00:00
|
|
|
if (events) {
|
2025-02-11 19:49:17 +00:00
|
|
|
const lastUserEvent = $events.find(e => e.pubkey === $pubkey)
|
|
|
|
|
|
|
|
|
|
// Adjust last checked to account for messages that came from a different device
|
|
|
|
|
const adjustedLastChecked =
|
|
|
|
|
lastChecked && lastUserEvent ? Math.max(lastUserEvent.created_at, lastChecked) : lastChecked
|
|
|
|
|
|
2025-01-28 16:13:20 +00:00
|
|
|
for (const event of $events.toReversed()) {
|
2025-02-03 23:40:00 +00:00
|
|
|
if (seen.has(event.id)) {
|
2025-01-28 16:13:20 +00:00
|
|
|
continue
|
|
|
|
|
}
|
2024-08-15 21:30:55 +00:00
|
|
|
|
2025-02-03 23:40:00 +00:00
|
|
|
const date = formatTimestampAsDate(event.created_at)
|
2024-08-15 21:30:55 +00:00
|
|
|
|
2025-01-28 22:44:43 +00:00
|
|
|
if (
|
|
|
|
|
!newMessagesSeen &&
|
2025-02-11 19:49:17 +00:00
|
|
|
adjustedLastChecked &&
|
2025-01-28 22:44:43 +00:00
|
|
|
event.pubkey !== $pubkey &&
|
2025-05-09 19:26:05 +00:00
|
|
|
event.created_at > adjustedLastChecked &&
|
|
|
|
|
event.created_at < mounted
|
2025-01-28 22:44:43 +00:00
|
|
|
) {
|
2025-01-28 22:19:26 +00:00
|
|
|
elements.push({type: "new-messages", id: "new-messages"})
|
|
|
|
|
newMessagesSeen = true
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-28 16:13:20 +00:00
|
|
|
if (date !== previousDate) {
|
|
|
|
|
elements.push({type: "date", value: date, id: date, showPubkey: false})
|
|
|
|
|
}
|
2024-08-15 21:30:55 +00:00
|
|
|
|
2025-01-28 16:13:20 +00:00
|
|
|
elements.push({
|
2025-02-03 23:40:00 +00:00
|
|
|
id: event.id,
|
2025-01-28 16:13:20 +00:00
|
|
|
type: "note",
|
|
|
|
|
value: event,
|
2025-11-14 00:39:44 +00:00
|
|
|
showPubkey:
|
|
|
|
|
previousPubkey !== event.pubkey ||
|
2025-12-04 22:27:43 +00:00
|
|
|
event.created_at - previousCreatedAt > int(3, MINUTE) ||
|
2025-11-14 00:39:44 +00:00
|
|
|
[ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER].includes(previousKind!),
|
2025-01-02 23:08:16 +00:00
|
|
|
})
|
2025-01-28 16:13:20 +00:00
|
|
|
|
|
|
|
|
previousDate = date
|
2025-11-14 00:39:44 +00:00
|
|
|
previousKind = event.kind
|
2025-02-03 23:40:00 +00:00
|
|
|
previousPubkey = event.pubkey
|
2025-12-04 22:27:43 +00:00
|
|
|
previousCreatedAt = event.created_at
|
2025-02-03 23:40:00 +00:00
|
|
|
seen.add(event.id)
|
2025-01-02 23:08:16 +00:00
|
|
|
}
|
2025-01-28 16:13:20 +00:00
|
|
|
}
|
2025-01-02 23:08:16 +00:00
|
|
|
|
2025-01-28 16:13:20 +00:00
|
|
|
elements.reverse()
|
2025-01-28 22:19:26 +00:00
|
|
|
|
|
|
|
|
setTimeout(onScroll, 100)
|
2025-02-04 00:37:14 +00:00
|
|
|
|
|
|
|
|
return elements
|
|
|
|
|
})
|
2024-11-15 22:03:54 +00:00
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
const start = () => {
|
|
|
|
|
cleanup?.()
|
|
|
|
|
|
|
|
|
|
const feed = makeFeed({
|
2025-10-06 18:23:19 +00:00
|
|
|
url,
|
2025-02-04 00:37:14 +00:00
|
|
|
element: element!,
|
2025-10-30 22:33:34 +00:00
|
|
|
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER, ROOM_REMOVE_MEMBER], "#h": [h]}],
|
2025-02-03 23:50:19 +00:00
|
|
|
onExhausted: () => {
|
2025-02-13 23:34:14 +00:00
|
|
|
loadingEvents = false
|
2025-02-03 23:50:19 +00:00
|
|
|
},
|
2025-05-28 23:46:41 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
events = feed.events
|
2026-02-15 02:27:36 +00:00
|
|
|
revealInFeed = feed.reveal
|
2025-05-28 23:46:41 +00:00
|
|
|
cleanup = feed.cleanup
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-05 00:59:17 +00:00
|
|
|
const onEscape = () => {
|
|
|
|
|
clearParent()
|
|
|
|
|
clearShare()
|
|
|
|
|
eventToEdit = undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-15 14:00:23 +00:00
|
|
|
const canEditEvent = (event: TrustedEvent) =>
|
|
|
|
|
event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)
|
|
|
|
|
|
|
|
|
|
const onEditEvent = (event: TrustedEvent) => {
|
|
|
|
|
clearParent()
|
|
|
|
|
clearShare()
|
|
|
|
|
eventToEdit = event
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onEditPrevious = () => {
|
|
|
|
|
const prev = $events.find(e => e.pubkey === $pubkey)
|
|
|
|
|
|
|
|
|
|
if (prev && canEditEvent(prev)) {
|
|
|
|
|
onEditEvent(prev)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-15 02:27:36 +00:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
onMount(() => {
|
2025-03-04 01:46:15 +00:00
|
|
|
const observer = new ResizeObserver(() => {
|
2025-05-08 18:05:27 +00:00
|
|
|
if (dynamicPadding && chatCompose) {
|
|
|
|
|
dynamicPadding!.style.minHeight = `${chatCompose!.offsetHeight}px`
|
|
|
|
|
}
|
2025-03-04 01:46:15 +00:00
|
|
|
})
|
|
|
|
|
|
2025-03-04 20:53:38 +00:00
|
|
|
observer.observe(chatCompose!)
|
2025-05-08 18:05:27 +00:00
|
|
|
observer.observe(dynamicPadding!)
|
2025-05-28 23:46:41 +00:00
|
|
|
start()
|
2025-03-04 01:46:15 +00:00
|
|
|
|
|
|
|
|
return () => {
|
2025-03-04 20:53:38 +00:00
|
|
|
observer.unobserve(chatCompose!)
|
2025-05-08 18:05:27 +00:00
|
|
|
observer.unobserve(dynamicPadding!)
|
2025-03-04 01:46:15 +00:00
|
|
|
}
|
2025-02-03 23:50:19 +00:00
|
|
|
})
|
2025-01-28 16:13:20 +00:00
|
|
|
|
2026-02-15 02:27:36 +00:00
|
|
|
$effect(() => {
|
|
|
|
|
const jumpId = $page.url.searchParams.get("jump")
|
|
|
|
|
|
|
|
|
|
if (!jumpId) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
void handleJump(jumpId)
|
|
|
|
|
}, 400)
|
|
|
|
|
})
|
|
|
|
|
|
2025-01-31 17:36:46 +00:00
|
|
|
onDestroy(() => {
|
2025-07-31 20:25:40 +00:00
|
|
|
cleanup?.()
|
2024-11-14 16:33:11 +00:00
|
|
|
})
|
2024-08-14 00:58:26 +00:00
|
|
|
</script>
|
|
|
|
|
|
2025-03-19 17:30:36 +00:00
|
|
|
<PageBar>
|
2025-03-04 01:46:15 +00:00
|
|
|
{#snippet icon()}
|
2025-11-06 00:59:17 +00:00
|
|
|
<RoomImage {url} {h} />
|
2025-03-04 01:46:15 +00:00
|
|
|
{/snippet}
|
|
|
|
|
{#snippet title()}
|
|
|
|
|
<div class="row-2">
|
2026-02-15 02:27:36 +00:00
|
|
|
<RoomName {url} {h} />
|
2025-11-10 22:59:15 +00:00
|
|
|
<Button
|
2026-02-15 02:27:36 +00:00
|
|
|
class="btn btn-neutral btn-xs tooltip tooltip-bottom"
|
2025-11-10 22:59:15 +00:00
|
|
|
data-tip="Room information"
|
|
|
|
|
onclick={showRoomDetail}>
|
|
|
|
|
<Icon size={4} icon={InfoCircle} />
|
|
|
|
|
</Button>
|
2026-02-15 02:27:36 +00:00
|
|
|
</div>
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet action()}
|
|
|
|
|
<div class="row-2 w-[10.5rem] min-w-0 shrink-0 sm:w-[14rem] md:w-auto">
|
|
|
|
|
<label class="input input-sm input-bordered flex min-w-0 w-full items-center gap-2 md:w-64">
|
|
|
|
|
<Icon size={4} icon={Magnifier} />
|
|
|
|
|
<input
|
|
|
|
|
bind:value={roomSearchTerm}
|
|
|
|
|
class="min-w-0 grow"
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Search space messages..."
|
|
|
|
|
onfocus={showRoomSearch}
|
|
|
|
|
oninput={onRoomSearchInput} />
|
|
|
|
|
</label>
|
|
|
|
|
<div class="shrink-0">
|
|
|
|
|
<SpaceMenuButton {url} />
|
|
|
|
|
</div>
|
2025-01-28 22:19:26 +00:00
|
|
|
</div>
|
2025-03-04 01:46:15 +00:00
|
|
|
{/snippet}
|
|
|
|
|
</PageBar>
|
|
|
|
|
|
2025-03-19 17:30:36 +00:00
|
|
|
<PageContent bind:element onscroll={onScroll} class="flex flex-col-reverse pt-4">
|
2025-03-04 01:46:15 +00:00
|
|
|
<div bind:this={dynamicPadding}></div>
|
2025-10-31 23:18:58 +00:00
|
|
|
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
2025-05-28 23:46:41 +00:00
|
|
|
<div class="py-20">
|
|
|
|
|
<div class="card2 col-8 m-auto max-w-md items-center text-center">
|
2025-10-31 19:10:16 +00:00
|
|
|
<p class="opacity-75">You aren't currently a member of this room.</p>
|
2025-10-31 23:18:58 +00:00
|
|
|
{#if !$room.isClosed}
|
2025-10-31 23:00:14 +00:00
|
|
|
{#if $membershipStatus === MembershipStatus.Pending}
|
|
|
|
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
|
|
|
|
<Icon icon={ClockCircle} />
|
|
|
|
|
Access Pending
|
|
|
|
|
</Button>
|
|
|
|
|
{:else}
|
|
|
|
|
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
|
|
|
|
{#if joining}
|
|
|
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
|
|
|
{:else}
|
|
|
|
|
<Icon icon={Login2} />
|
|
|
|
|
{/if}
|
|
|
|
|
Join Room
|
|
|
|
|
</Button>
|
|
|
|
|
{/if}
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
2025-03-04 01:46:15 +00:00
|
|
|
</div>
|
2025-05-28 23:46:41 +00:00
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
{#each elements as { type, id, value, showPubkey } (id)}
|
|
|
|
|
{#if type === "new-messages"}
|
|
|
|
|
<div
|
2025-12-17 20:07:30 +00:00
|
|
|
{id}
|
2025-05-28 23:46:41 +00:00
|
|
|
class="flex items-center py-2 text-xs transition-colors"
|
|
|
|
|
class:opacity-0={showFixedNewMessages}>
|
|
|
|
|
<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>
|
|
|
|
|
<div class="h-px flex-grow bg-primary"></div>
|
|
|
|
|
</div>
|
|
|
|
|
{:else if type === "date"}
|
|
|
|
|
<Divider>{value}</Divider>
|
|
|
|
|
{:else}
|
2025-10-15 16:04:17 +00:00
|
|
|
{@const event = $state.snapshot(value as TrustedEvent)}
|
|
|
|
|
{#if event.kind === ROOM_ADD_MEMBER}
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomItemAddMember {url} {event} />
|
2025-10-15 16:04:17 +00:00
|
|
|
{:else if event.kind === ROOM_REMOVE_MEMBER}
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomItemRemoveMember {url} {event} />
|
2025-10-15 16:04:17 +00:00
|
|
|
{:else}
|
|
|
|
|
<div in:slide class:-mt-1={!showPubkey}>
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomItem
|
2025-10-15 16:04:17 +00:00
|
|
|
{url}
|
|
|
|
|
{event}
|
|
|
|
|
{replyTo}
|
|
|
|
|
{showPubkey}
|
|
|
|
|
canEdit={canEditEvent}
|
|
|
|
|
onEdit={onEditEvent} />
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
|
|
|
|
{/each}
|
|
|
|
|
<p class="flex h-10 items-center justify-center py-20">
|
|
|
|
|
{#if loadingEvents}
|
|
|
|
|
<Spinner loading={loadingEvents}>Looking for messages...</Spinner>
|
|
|
|
|
{:else}
|
|
|
|
|
<Spinner>End of message history</Spinner>
|
|
|
|
|
{/if}
|
|
|
|
|
</p>
|
|
|
|
|
{/if}
|
2025-03-19 17:30:36 +00:00
|
|
|
</PageContent>
|
2025-03-04 01:46:15 +00:00
|
|
|
|
2026-02-15 02:27:36 +00:00
|
|
|
{#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}
|
|
|
|
|
|
2025-03-04 20:53:38 +00:00
|
|
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
2025-10-31 23:18:58 +00:00
|
|
|
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
|
2025-05-28 23:46:41 +00:00
|
|
|
<!-- pass -->
|
2025-10-31 23:18:58 +00:00
|
|
|
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
2025-05-29 17:42:43 +00:00
|
|
|
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
2025-11-03 19:10:16 +00:00
|
|
|
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
2025-10-31 23:18:58 +00:00
|
|
|
{#if !$room.isClosed}
|
2025-10-31 23:00:14 +00:00
|
|
|
{#if $membershipStatus === MembershipStatus.Pending}
|
|
|
|
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
|
|
|
|
<Icon icon={ClockCircle} />
|
|
|
|
|
Access Pending
|
|
|
|
|
</Button>
|
|
|
|
|
{:else}
|
|
|
|
|
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
|
|
|
|
{#if joining}
|
|
|
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
|
|
|
{:else}
|
|
|
|
|
<Icon icon={Login2} />
|
|
|
|
|
{/if}
|
|
|
|
|
Ask to Join
|
|
|
|
|
</Button>
|
|
|
|
|
{/if}
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div>
|
|
|
|
|
{#if parent}
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
|
|
|
|
{#if share}
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
2025-10-15 14:00:23 +00:00
|
|
|
{#if eventToEdit}
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomComposeEdit clear={clearEventToEdit} />
|
2025-10-15 14:00:23 +00:00
|
|
|
{/if}
|
2025-05-28 23:46:41 +00:00
|
|
|
</div>
|
2025-10-15 14:00:23 +00:00
|
|
|
{#key eventToEdit}
|
2025-10-30 22:36:14 +00:00
|
|
|
<RoomCompose
|
2025-10-15 14:00:23 +00:00
|
|
|
{url}
|
2025-10-30 22:33:34 +00:00
|
|
|
{h}
|
2025-10-06 18:23:19 +00:00
|
|
|
{onSubmit}
|
2025-11-05 00:59:17 +00:00
|
|
|
{onEscape}
|
2025-10-06 18:23:19 +00:00
|
|
|
{onEditPrevious}
|
|
|
|
|
content={eventToEdit?.content}
|
|
|
|
|
bind:this={compose} />
|
2025-10-15 14:00:23 +00:00
|
|
|
{/key}
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
2024-08-15 19:08:07 +00:00
|
|
|
</div>
|
2025-01-28 16:13:20 +00:00
|
|
|
|
|
|
|
|
{#if showScrollButton}
|
2025-03-04 01:46:15 +00:00
|
|
|
<div in:fade class="chat__scroll-down">
|
2025-02-04 01:21:46 +00:00
|
|
|
<Button class="btn btn-circle btn-neutral" onclick={scrollToBottom}>
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={AltArrowDown} />
|
2025-01-28 16:13:20 +00:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
2025-03-04 01:46:15 +00:00
|
|
|
|
|
|
|
|
{#if showFixedNewMessages}
|
2025-11-26 01:06:21 +00:00
|
|
|
<div class="relative z-popover flex justify-center">
|
2025-03-04 01:46:15 +00:00
|
|
|
<div transition:fly={{duration: 200}} class="fixed top-12">
|
|
|
|
|
<Button class="btn btn-primary btn-xs rounded-full" onclick={scrollToNewMessages}>
|
|
|
|
|
New Messages
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|