2024-08-14 00:58:26 +00:00
|
|
|
<script lang="ts">
|
2025-05-29 17:42:43 +00:00
|
|
|
import cx from "classnames"
|
2025-02-04 00:37:14 +00:00
|
|
|
import {readable} from "svelte/store"
|
2025-02-03 23:50:19 +00:00
|
|
|
import {onMount, onDestroy} from "svelte"
|
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"
|
2025-05-05 17:11:02 +00:00
|
|
|
import {now, formatTimestampAsDate} from "@welshman/lib"
|
2025-05-28 23:46:41 +00:00
|
|
|
import {request} from "@welshman/net"
|
2024-10-01 18:47:21 +00:00
|
|
|
import type {TrustedEvent, EventContent} from "@welshman/util"
|
2025-05-29 17:42:43 +00:00
|
|
|
import {
|
2025-06-10 20:35:57 +00:00
|
|
|
makeEvent,
|
2025-06-16 17:12:24 +00:00
|
|
|
makeRoomMeta,
|
2025-05-29 17:42:43 +00:00
|
|
|
MESSAGE,
|
|
|
|
|
DELETE,
|
2025-06-16 17:12:24 +00:00
|
|
|
ROOM_ADD_USER,
|
|
|
|
|
ROOM_REMOVE_USER,
|
2025-05-29 17:42:43 +00:00
|
|
|
} from "@welshman/util"
|
2025-09-06 00:32:01 +00:00
|
|
|
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
|
2025-01-28 22:19:26 +00:00
|
|
|
import {slide, fade, fly} from "@lib/transition"
|
2025-09-11 19:43:18 +00:00
|
|
|
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
|
2025-09-10 22:26:39 +00:00
|
|
|
import ClockCircle from "@assets/icons/clock-circle.svg?dataurl"
|
2025-09-12 16:11:53 +00:00
|
|
|
import Login2 from "@assets/icons/login-3.svg?dataurl"
|
2025-09-10 22:26:39 +00:00
|
|
|
import AltArrowDown from "@assets/icons/alt-arrow-down.svg?dataurl"
|
|
|
|
|
import Logout2 from "@assets/icons/logout-3.svg?dataurl"
|
|
|
|
|
import Bookmark from "@assets/icons/bookmark.svg?dataurl"
|
2024-08-16 22:17:30 +00:00
|
|
|
import Icon from "@lib/components/Icon.svelte"
|
2024-09-11 19:19:45 +00:00
|
|
|
import Button from "@lib/components/Button.svelte"
|
2024-08-16 17:50:38 +00:00
|
|
|
import Spinner from "@lib/components/Spinner.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"
|
2024-09-20 18:00:41 +00:00
|
|
|
import Divider from "@lib/components/Divider.svelte"
|
2025-08-19 21:03:04 +00:00
|
|
|
import ThunkToast from "@app/components/ThunkToast.svelte"
|
2024-11-15 22:03:54 +00:00
|
|
|
import MenuSpaceButton from "@app/components/MenuSpaceButton.svelte"
|
2024-12-10 01:04:39 +00:00
|
|
|
import ChannelName from "@app/components/ChannelName.svelte"
|
2024-10-03 21:43:17 +00:00
|
|
|
import ChannelMessage from "@app/components/ChannelMessage.svelte"
|
|
|
|
|
import ChannelCompose from "@app/components/ChannelCompose.svelte"
|
2025-01-28 17:20:34 +00:00
|
|
|
import ChannelComposeParent from "@app/components/ChannelComposeParent.svelte"
|
2024-10-08 18:39:16 +00:00
|
|
|
import {
|
2025-05-28 23:46:41 +00:00
|
|
|
userRoomsByUrl,
|
2025-09-03 16:02:54 +00:00
|
|
|
userSettingsValues,
|
2024-10-21 17:30:12 +00:00
|
|
|
decodeRelay,
|
2025-01-28 16:13:20 +00:00
|
|
|
getEventsForUrl,
|
2025-05-28 23:46:41 +00:00
|
|
|
deriveUserMembershipStatus,
|
|
|
|
|
deriveChannel,
|
|
|
|
|
MembershipStatus,
|
2025-07-17 21:39:59 +00:00
|
|
|
REACTION_KINDS,
|
2025-08-21 22:01:31 +00:00
|
|
|
} from "@app/core/state"
|
|
|
|
|
import {setChecked, checked} from "@app/util/notifications"
|
2025-07-30 23:30:03 +00:00
|
|
|
import {
|
|
|
|
|
addRoomMembership,
|
|
|
|
|
canEnforceNip70,
|
|
|
|
|
removeRoomMembership,
|
|
|
|
|
prependParent,
|
2025-08-21 22:01:31 +00:00
|
|
|
} from "@app/core/commands"
|
|
|
|
|
import {PROTECTED} from "@app/core/state"
|
|
|
|
|
import {makeFeed} from "@app/core/requests"
|
|
|
|
|
import {popKey} from "@lib/implicit"
|
|
|
|
|
import {pushToast} from "@app/util/toast"
|
2024-08-15 19:08:07 +00:00
|
|
|
|
2025-05-27 22:04:44 +00:00
|
|
|
const {room} = $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]
|
2024-10-21 17:30:12 +00:00
|
|
|
const url = decodeRelay($page.params.relay)
|
2025-05-28 23:46:41 +00:00
|
|
|
const channel = deriveChannel(url, room)
|
2025-01-28 16:13:20 +00:00
|
|
|
const filter = {kinds: [MESSAGE], "#h": [room]}
|
2025-05-28 23:46:41 +00:00
|
|
|
const isFavorite = $derived($userRoomsByUrl.get(url)?.has(room))
|
2025-08-19 21:03:04 +00:00
|
|
|
const shouldProtect = canEnforceNip70(url)
|
2025-05-28 23:46:41 +00:00
|
|
|
const membershipStatus = deriveUserMembershipStatus(url, room)
|
2025-01-02 23:08:16 +00:00
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
const addFavorite = () => addRoomMembership(url, room)
|
2025-02-13 23:34:14 +00:00
|
|
|
|
2025-05-28 23:46:41 +00:00
|
|
|
const removeFavorite = () => removeRoomMembership(url, room)
|
2024-12-10 17:44:04 +00:00
|
|
|
|
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-09-06 00:32:01 +00:00
|
|
|
const message = await waitForThunkError(joinRoom(url, makeRoomMeta({id: room})))
|
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})
|
|
|
|
|
} else {
|
|
|
|
|
// Restart the feed now that we're a member
|
|
|
|
|
start()
|
|
|
|
|
}
|
|
|
|
|
} 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-09-06 00:32:01 +00:00
|
|
|
const message = await waitForThunkError(leaveRoom(url, makeRoomMeta({id: room})))
|
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-07-30 23:30:03 +00:00
|
|
|
const onSubmit = async ({content, tags}: EventContent) => {
|
2025-06-13 22:17:20 +00:00
|
|
|
tags.push(["h", room])
|
2025-07-30 23:30:03 +00:00
|
|
|
|
2025-08-19 21:03:04 +00:00
|
|
|
if (await shouldProtect) {
|
2025-07-30 23:30:03 +00:00
|
|
|
tags.push(PROTECTED)
|
|
|
|
|
}
|
2025-01-28 16:13:20 +00:00
|
|
|
|
2025-02-06 19:33:01 +00:00
|
|
|
let template = {content, tags}
|
|
|
|
|
|
|
|
|
|
if (share) {
|
|
|
|
|
template = prependParent(share, template)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parent) {
|
|
|
|
|
template = prependParent(parent, template)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-19 21:03:04 +00:00
|
|
|
const thunk = publishThunk({
|
2024-11-12 22:32:36 +00:00
|
|
|
relays: [url],
|
2025-06-10 20:35:57 +00:00
|
|
|
event: makeEvent(MESSAGE, template),
|
2025-09-03 16:02:54 +00:00
|
|
|
delay: $userSettingsValues.send_delay,
|
2024-11-12 22:32:36 +00:00
|
|
|
})
|
2025-01-28 17:20:34 +00:00
|
|
|
|
2025-08-19 21:03:04 +00:00
|
|
|
pushToast({
|
|
|
|
|
timeout: 30_000,
|
|
|
|
|
children: {
|
|
|
|
|
component: ThunkToast,
|
|
|
|
|
props: {thunk},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2025-01-28 17:20:34 +00:00
|
|
|
clearParent()
|
2025-02-06 19:33:01 +00:00
|
|
|
clearShare()
|
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
|
|
|
|
|
|
|
|
|
|
if (!newMessages || newMessagesSeen) {
|
|
|
|
|
showFixedNewMessages = false
|
|
|
|
|
} else {
|
|
|
|
|
const {y} = newMessages.getBoundingClientRect()
|
|
|
|
|
|
|
|
|
|
if (y > 300) {
|
|
|
|
|
newMessagesSeen = true
|
|
|
|
|
} else {
|
|
|
|
|
showFixedNewMessages = y < 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const scrollToNewMessages = () =>
|
2025-02-04 00:37:14 +00:00
|
|
|
newMessages?.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()
|
|
|
|
|
let newMessages: 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([]))
|
|
|
|
|
let compose: ChannelCompose | undefined = $state()
|
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
|
|
|
|
|
let previousPubkey
|
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-02-03 23:40:00 +00:00
|
|
|
showPubkey: date !== previousDate || previousPubkey !== event.pubkey,
|
2025-01-02 23:08:16 +00:00
|
|
|
})
|
2025-01-28 16:13:20 +00:00
|
|
|
|
|
|
|
|
previousDate = date
|
2025-02-03 23:40:00 +00:00
|
|
|
previousPubkey = event.pubkey
|
|
|
|
|
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-02-04 00:37:14 +00:00
|
|
|
element: element!,
|
2025-02-03 23:50:19 +00:00
|
|
|
relays: [url],
|
|
|
|
|
feedFilters: [filter],
|
2025-07-04 13:22:19 +00:00
|
|
|
subscriptionFilters: [
|
2025-07-17 21:39:59 +00:00
|
|
|
{kinds: [DELETE, MESSAGE, ...REACTION_KINDS], "#h": [room], since: now()},
|
2025-07-04 13:22:19 +00:00
|
|
|
],
|
2025-02-06 00:26:22 +00:00
|
|
|
initialEvents: getEventsForUrl(url, [{...filter, limit: 20}]),
|
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
|
|
|
|
|
cleanup = feed.cleanup
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onMount(() => {
|
|
|
|
|
const controller = new AbortController()
|
|
|
|
|
|
2025-05-29 17:42:43 +00:00
|
|
|
request({
|
2025-05-28 23:46:41 +00:00
|
|
|
signal: controller.signal,
|
|
|
|
|
relays: [url],
|
2025-05-29 17:42:43 +00:00
|
|
|
filters: [
|
|
|
|
|
{
|
2025-06-16 17:12:24 +00:00
|
|
|
kinds: [ROOM_ADD_USER, ROOM_REMOVE_USER],
|
2025-05-29 17:42:43 +00:00
|
|
|
"#p": [$pubkey!],
|
|
|
|
|
"#h": [room],
|
|
|
|
|
limit: 10,
|
|
|
|
|
},
|
|
|
|
|
],
|
2025-05-28 23:46:41 +00:00
|
|
|
})
|
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-05-28 23:46:41 +00:00
|
|
|
controller.abort()
|
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
|
|
|
|
2025-01-31 17:36:46 +00:00
|
|
|
onDestroy(() => {
|
2025-07-31 20:25:40 +00:00
|
|
|
cleanup?.()
|
2025-02-06 19:39:51 +00:00
|
|
|
|
|
|
|
|
// Sveltekit calls onDestroy at the beginning of the page load for some reason
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
setChecked($page.url.pathname)
|
2025-02-24 21:40:26 +00:00
|
|
|
}, 800)
|
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()}
|
|
|
|
|
<div class="center">
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={Hashtag} />
|
2025-03-04 01:46:15 +00:00
|
|
|
</div>
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet title()}
|
2025-03-04 18:00:24 +00:00
|
|
|
<strong class="ellipsize">
|
2025-03-04 01:46:15 +00:00
|
|
|
<ChannelName {url} {room} />
|
|
|
|
|
</strong>
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet action()}
|
|
|
|
|
<div class="row-2">
|
2025-05-29 20:37:23 +00:00
|
|
|
{#if $membershipStatus === MembershipStatus.Initial}
|
|
|
|
|
<Button
|
|
|
|
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
|
|
|
|
data-tip="Request to be added to the member list"
|
|
|
|
|
disabled={joining}
|
|
|
|
|
onclick={join}>
|
|
|
|
|
{#if joining}
|
|
|
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
|
|
|
{:else}
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon size={4} icon={Login2} />
|
2025-05-29 20:37:23 +00:00
|
|
|
{/if}
|
|
|
|
|
</Button>
|
|
|
|
|
{:else if $membershipStatus === MembershipStatus.Pending}
|
|
|
|
|
<Button
|
|
|
|
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
|
|
|
|
data-tip="Membership is pending">
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon size={4} icon={ClockCircle} />
|
2025-05-29 20:37:23 +00:00
|
|
|
</Button>
|
|
|
|
|
{:else}
|
2025-05-29 17:42:43 +00:00
|
|
|
<Button
|
|
|
|
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
|
|
|
|
data-tip="Request to be removed from member list"
|
|
|
|
|
disabled={leaving}
|
|
|
|
|
onclick={leave}>
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon size={4} icon={Logout2} />
|
2025-05-29 17:42:43 +00:00
|
|
|
</Button>
|
|
|
|
|
{/if}
|
2025-05-28 23:46:41 +00:00
|
|
|
<Button
|
|
|
|
|
class="btn btn-neutral btn-sm tooltip tooltip-left"
|
|
|
|
|
data-tip={isFavorite ? "Remove Favorite" : "Add Favorite"}
|
|
|
|
|
onclick={isFavorite ? removeFavorite : addFavorite}>
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon size={4} icon={Bookmark} class={cx({"text-primary": isFavorite})} />
|
2025-05-28 23:46:41 +00:00
|
|
|
</Button>
|
2025-03-04 01:46:15 +00:00
|
|
|
<MenuSpaceButton {url} />
|
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-05-28 23:46:41 +00:00
|
|
|
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
|
|
|
|
|
<div class="py-20">
|
|
|
|
|
<div class="card2 col-8 m-auto max-w-md items-center text-center">
|
2025-05-29 17:42:43 +00:00
|
|
|
<p class="row-2">You aren't currently a member of this room.</p>
|
2025-05-28 23:46:41 +00:00
|
|
|
{#if $membershipStatus === MembershipStatus.Pending}
|
|
|
|
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={ClockCircle} />
|
2025-05-28 23:46:41 +00:00
|
|
|
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}
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={Login2} />
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
|
|
|
|
Join Room
|
|
|
|
|
</Button>
|
|
|
|
|
{/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
|
|
|
|
|
bind:this={newMessages}
|
|
|
|
|
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}
|
|
|
|
|
<div in:slide class:-mt-1={!showPubkey}>
|
|
|
|
|
<ChannelMessage
|
|
|
|
|
{url}
|
|
|
|
|
{replyTo}
|
|
|
|
|
event={$state.snapshot(value as TrustedEvent)}
|
|
|
|
|
{showPubkey} />
|
|
|
|
|
</div>
|
|
|
|
|
{/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
|
|
|
|
2025-03-04 20:53:38 +00:00
|
|
|
<div class="chat__compose bg-base-200" bind:this={chatCompose}>
|
2025-05-28 23:46:41 +00:00
|
|
|
{#if $channel?.private && $membershipStatus !== MembershipStatus.Granted}
|
|
|
|
|
<!-- pass -->
|
|
|
|
|
{:else if $channel?.closed && $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-05-28 23:46:41 +00:00
|
|
|
<p>Only members are allowed to post to this room.</p>
|
|
|
|
|
{#if $membershipStatus === MembershipStatus.Pending}
|
|
|
|
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={ClockCircle} />
|
2025-05-28 23:46:41 +00:00
|
|
|
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}
|
2025-09-10 22:26:39 +00:00
|
|
|
<Icon icon={Login2} />
|
2025-05-28 23:46:41 +00:00
|
|
|
{/if}
|
|
|
|
|
Ask to Join
|
|
|
|
|
</Button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
{:else}
|
|
|
|
|
<div>
|
|
|
|
|
{#if parent}
|
|
|
|
|
<ChannelComposeParent event={parent} clear={clearParent} verb="Replying to" />
|
|
|
|
|
{/if}
|
|
|
|
|
{#if share}
|
|
|
|
|
<ChannelComposeParent event={share} clear={clearShare} verb="Sharing" />
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
<ChannelCompose bind:this={compose} {onSubmit} {url} />
|
|
|
|
|
{/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}
|
|
|
|
|
<div class="relative z-feature flex justify-center">
|
|
|
|
|
<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}
|