flotilla/src/routes/spaces/[relay]/[h]/+page.svelte

594 lines
18 KiB
Svelte
Raw Normal View History

2024-08-14 00:58:26 +00:00
<script lang="ts">
import {onMount} from "svelte"
import {readable} from "svelte/store"
2024-08-16 17:50:38 +00:00
import {page} from "$app/stores"
2026-02-16 23:28:25 +00:00
import {goto} from "$app/navigation"
2025-01-28 16:13:20 +00:00
import type {Readable} from "svelte/store"
import {pubkey, publishThunk, waitForThunkError, joinRoom, leaveRoom} from "@welshman/app"
2026-03-10 22:13:33 +00:00
import {now, ifLet, int, formatTimestampAsDate, ago, MINUTE} 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"
import {makeEvent, makeRoomMeta, MESSAGE, ROOM_ADD_MEMBER} from "@welshman/util"
2026-02-15 02:27:36 +00:00
import AltArrowDown from "@assets/icons/alt-arrow-down.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"
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
import cx from "classnames"
2026-02-15 02:27:36 +00:00
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"
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 SpaceBar from "@app/components/SpaceBar.svelte"
2026-02-15 02:27:36 +00:00
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"
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 SpaceSearch from "@app/components/SpaceSearch.svelte"
2026-02-15 02:27:36 +00:00
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 {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,
2025-10-30 22:36:14 +00:00
deriveRoom,
2026-02-15 02:27:36 +00:00
deriveUserRoomMembershipStatus,
getRoomType,
2026-02-15 02:27:36 +00:00
MESSAGE_KINDS,
2025-05-28 23:46:41 +00:00
MembershipStatus,
2025-10-06 18:23:19 +00:00
PROTECTED,
RoomType,
2026-01-30 16:41:58 +00:00
userSettingsValues,
} from "@app/core/state"
import VoiceWidget from "@app/components/VoiceWidget.svelte"
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
import VideoCallContent from "@app/components/VideoCallContent.svelte"
import {VoiceState, currentVoiceRoom, voiceState} from "@app/call/stores"
import {VideoCallLayout, videoCallLayout, videoTileCount} from "@app/call/video"
import {makeFeed} from "@app/core/requests"
import {popKey} from "@lib/implicit"
2026-02-15 02:27:36 +00:00
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 {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()
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)
const isVoiceRoom = $derived(getRoomType($room) === RoomType.Voice)
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
const voiceConnectedHere = $derived(
isVoiceRoom &&
$voiceState === VoiceState.Connected &&
$currentVoiceRoom?.url === url &&
$currentVoiceRoom?.h === h,
)
const showMobileVideoPanel = $derived(
isVoiceRoom &&
$voiceState === VoiceState.Connected &&
$videoCallLayout === VideoCallLayout.Video,
)
const pageContentHiddenDesktopVideoOnly = $derived(
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video,
)
let prevVideoTileCount = $state(0)
$effect(() => {
if ($voiceState !== VoiceState.Connected) {
videoCallLayout.set(VideoCallLayout.Chat)
prevVideoTileCount = 0
return
}
const here = isVoiceRoom && $currentVoiceRoom?.url === url && $currentVoiceRoom?.h === h
const n = $videoTileCount
if (!here) {
prevVideoTileCount = 0
return
}
if (prevVideoTileCount === 0 && n >= 1) {
videoCallLayout.set(VideoCallLayout.Video)
}
if (prevVideoTileCount >= 1 && n === 0 && $videoCallLayout === VideoCallLayout.Split) {
videoCallLayout.set(VideoCallLayout.Chat)
}
prevVideoTileCount = n
})
const shouldProtect = canEnforceNip70(url)
2025-10-30 22:33:34 +00:00
const membershipStatus = deriveUserRoomMembershipStatus(url, h)
2026-02-17 17:43:32 +00:00
const at = $derived(parseInt($page.url.searchParams.get("at")!))
2025-01-02 23:08:16 +00:00
const showRoomDetail = () => pushModal(RoomDetail, {url, h})
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})
}
// 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
}
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
}
2025-01-28 16:13:20 +00:00
const clearParent = () => {
parent = undefined
}
2025-02-06 19:33:01 +00:00
const clearShare = () => {
share = undefined
}
const clearEventToEdit = () => {
eventToEdit = undefined
}
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)
}
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,
})
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
const manageScrollPosition = () => {
2026-02-17 17:43:32 +00:00
showScrollButton = !isNaN(at) || Math.abs(element?.scrollTop || 0) > 1500
2025-12-17 20:07:30 +00:00
const newMessages = document.getElementById("new-messages")
2026-03-30 20:22:22 +00:00
if (newMessagesSeen) {
showFixedNewMessages = false
2026-03-30 20:22:22 +00:00
} else if (newMessages) {
const {y} = newMessages.getBoundingClientRect()
2026-03-30 20:22:22 +00:00
if (y > 0 && y < 300) {
newMessagesSeen = true
2026-03-30 20:22:22 +00:00
showFixedNewMessages = false
} else {
showFixedNewMessages = y < 0
}
}
2026-02-17 17:43:32 +00:00
if (!userHasScrolled && !isNaN(at)) {
const targetEvent = $events.find(event => event.created_at >= at)
if (targetEvent) {
const target = element?.querySelector(`[data-event="${targetEvent.id}"]`)
if (target instanceof HTMLElement) {
isProgrammaticScroll = true
target.scrollIntoView({block: "center"})
}
}
}
}
const onScroll = () => {
if (!isProgrammaticScroll) {
userHasScrolled = true
manageScrollPosition()
}
isProgrammaticScroll = false
}
const scrollToNewMessages = () =>
2025-12-17 20:07:30 +00:00
document.getElementById("new-messages")?.scrollIntoView({behavior: "smooth", block: "center"})
2026-02-16 21:31:43 +00:00
const scrollToBottom = () => {
2026-02-17 17:43:32 +00:00
if (!isNaN(at)) {
2026-02-16 23:28:25 +00:00
goto($page.url.pathname, {replaceState: true})
2026-02-16 21:31:43 +00:00
} else {
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)
let userHasScrolled = $state(false)
let isProgrammaticScroll = $state(false)
2026-02-16 21:31:43 +00:00
let loadingBackward = $state(true)
let loadingForward = $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 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([]))
2025-10-30 22:36:14 +00:00
let compose: RoomCompose | undefined = $state()
let eventToEdit: TrustedEvent | 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
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
let newMessagesSeen = false
2024-08-15 21:30:55 +00:00
2025-01-28 16:13:20 +00:00
if (events) {
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
2026-02-16 21:31:43 +00:00
for (const event of $events) {
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 &&
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
) {
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
const showPubkey =
previousPubkey !== event.pubkey ||
event.created_at - previousCreatedAt > int(3, MINUTE) ||
previousKind === ROOM_ADD_MEMBER
if (showPubkey && elements.length > 0) {
elements[elements.length - 1].addSpaceBelow = true
}
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,
showPubkey,
addSpaceBelow: false,
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
}
if (elements.length > 0) {
elements[elements.length - 1].addSpaceBelow = true
}
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()
requestAnimationFrame(manageScrollPosition)
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,
2026-02-17 17:43:32 +00:00
at: at || now(),
2025-02-04 00:37:14 +00:00
element: element!,
filters: [{kinds: [...MESSAGE_KINDS, ROOM_ADD_MEMBER], "#h": [h]}],
2026-02-16 21:31:43 +00:00
onBackwardExhausted: () => {
loadingBackward = false
},
onForwardExhausted: () => {
loadingForward = false
2025-02-03 23:50:19 +00:00
},
2025-05-28 23:46:41 +00:00
})
events = feed.events
cleanup = feed.cleanup
}
2025-11-05 00:59:17 +00:00
const onEscape = () => {
clearParent()
clearShare()
eventToEdit = undefined
}
const canEditEvent = (event: TrustedEvent) =>
event.pubkey === $pubkey && event.created_at >= ago(5, MINUTE)
const onEditEvent = (event: TrustedEvent) => {
clearParent()
clearShare()
eventToEdit = event
}
2026-03-10 22:13:33 +00:00
const onEditPrevious = () => ifLet($events.toReversed().find(canEditEvent), onEditEvent)
2025-05-28 23:46:41 +00:00
onMount(() => {
start()
2026-04-04 16:02:52 +00:00
return cleanup
2025-02-03 23:50:19 +00:00
})
2024-08-14 00:58:26 +00:00
</script>
<SpaceBar>
{#snippet title()}
2026-03-16 20:39:47 +00:00
<RoomImage {url} {h} />
<RoomName {url} {h} />
2026-02-15 02:27:36 +00:00
{/snippet}
{#snippet action()}
<SpaceSearch {url} {h} />
<Button class="btn btn-neutral btn-sm btn-square" onclick={showRoomDetail}>
<Icon size={4} icon={InfoCircle} />
</Button>
{/snippet}
</SpaceBar>
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
<div
class={cx(
"flex min-h-0 flex-1 flex-col",
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Split && "md:flex-row",
)}>
{#if voiceConnectedHere}
<VideoCallContent
layout={$videoCallLayout}
{url}
{h}
class="hidden min-h-0 w-full min-w-0 flex-1 flex-col md:flex" />
{/if}
<div
class={cx(
"flex min-h-0 min-w-0 flex-1 flex-col",
voiceConnectedHere && $videoCallLayout === VideoCallLayout.Video && "md:hidden",
)}>
{#if isVoiceRoom && $voiceState === VoiceState.Connected}
<VideoCallContent layout={$videoCallLayout} mobile {url} {h} class="md:hidden" />
{/if}
<PageContent
bind:element
onscroll={onScroll}
class={cx(
showMobileVideoPanel
? "hidden flex-col-reverse pt-4 md:flex md:flex-col-reverse"
: "flex flex-col-reverse pt-4",
pageContentHiddenDesktopVideoOnly && "md:hidden",
)}>
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
<div class="py-20">
<div class="card2 col-8 m-auto max-w-md items-center text-center">
<p class="opacity-75">You aren't currently a member of this room.</p>
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
<Icon icon={ClockCircle} />
Access Pending
</Button>
{:else}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
<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}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
</div>
2025-05-28 23:46:41 +00:00
</div>
{:else}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
{#if loadingForward}
<p class="py-20 flex justify-center">
<Spinner loading={loadingForward}>Looking for messages...</Spinner>
</p>
{/if}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
{#each elements as { type, id, value, showPubkey, addSpaceBelow } (id)}
{#if type === "new-messages"}
<div
{id}
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}
{@const event = $state.snapshot(value as TrustedEvent)}
{#if event.kind === ROOM_ADD_MEMBER}
<RoomItemAddMember {url} {event} />
{:else}
<div in:slide class="cv">
<RoomItem
{url}
{event}
{replyTo}
{showPubkey}
{addSpaceBelow}
canEdit={canEditEvent}
onEdit={onEditEvent} />
</div>
{/if}
{/if}
{/each}
<p class="flex h-10 items-center justify-center py-20">
{#if loadingBackward}
<Spinner loading={loadingBackward}>Looking for messages...</Spinner>
{:else}
<Spinner>End of message history</Spinner>
{/if}
</p>
2025-05-28 23:46:41 +00:00
{/if}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
<div class="h-screen"></div>
</PageContent>
<div
class={cx(
"chat__compose-zone chat__compose flex flex-col gap-1 bg-base-200 md:flex-row md:gap-0",
pageContentHiddenDesktopVideoOnly && "md:hidden",
showMobileVideoPanel && "max-md:hidden",
)}>
<div class="chat__compose-inner min-w-0 flex-1">
{#if $room.isPrivate && $membershipStatus !== MembershipStatus.Granted}
<!-- pass -->
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
<p class="opacity-75">Only members are allowed to post to this room.</p>
{#if $membershipStatus === MembershipStatus.Pending}
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
<Icon icon={ClockCircle} />
Access Pending
</Button>
{:else}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
<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}
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
</div>
{:else}
<div>
{#if parent}
<RoomComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if}
{#if share}
<RoomComposeParent event={share} clear={clearShare} verb="Sharing" />
{/if}
{#if eventToEdit}
<RoomComposeEdit clear={clearEventToEdit} />
{/if}
</div>
{#key eventToEdit}
<RoomCompose
{url}
{h}
{onSubmit}
{onEscape}
{onEditPrevious}
initialValues={eventToEdit}
bind:this={compose} />
{/key}
{/if}
</div>
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
{#if isVoiceRoom || $voiceState === VoiceState.Joining || $voiceState === VoiceState.Connected}
<div
class={cx(
"hide-on-keyboard flex-shrink-0 p-2 md:hidden",
showMobileVideoPanel && "hidden",
)}>
<VoiceWidget />
</div>
{/if}
2025-05-28 23:46:41 +00:00
</div>
Video in calls (#135) #135 This PR adds basic video functionality to our voice rooms. Again I followed the Discord UX for inspiration, so all video calls start as voice-only calls that gracefully upgrade (and downgrade) when someone turns on a video or starts screen sharing. When a video feed is detected the Room page will change to display a grid of feeds. The grid logic is very basic, that's definitely an area to improve in the future. You can open the chat part of the room with a new button on the VoiceWidget - on the desktop layout this creates a split view with video on the left and chat on the right, but on mobile it switches to chat fullscreen. I also added a little pin icon you can use to focus on a single video feed (useful for screen sharing). There is a lot of tailwind I don't understand here, but it seems to work well enough. I moved voice.ts into a new `call` folder and moved some of its stores into `call/stores.ts` which allowed me to keep most of the video logic in `call/video.ts`. It's not a perfect encapsulation as voice.ts does subscribe to some of the hooks for the livekit calls and passes some of the signals onto `video.ts`. This could probably be broken up better but for this PR I'd rather not focus on making it perfect if that's ok. Partly for the sake of time but also because I envision another PR that renames/reorganizes things and I think a larger UX evaluation is necessary and should include real user feedback. I'm not confident tha""t the Voice Room concept as a whole will stick going forward. Maybe all rooms in a livekit enabled server should be able to host a call (like a slack huddle), maybe users want to be able to schedule calls as events, or even have them start with an ad-hoc set of participants completely outside of a NIP-29 group, etc. Co-authored-by: mplorentz <mplorentz@noreply.gitea.coracle.social> Reviewed-on: https://gitea.coracle.social/coracle/flotilla/pulls/135 Co-authored-by: Matt Lorentz <mplorentz@noreply.coracle.social> Co-committed-by: Matt Lorentz <mplorentz@noreply.coracle.social>
2026-04-08 17:10:20 +00:00
</div>
2024-08-15 19:08:07 +00:00
</div>
2025-01-28 16:13:20 +00:00
{#if showScrollButton}
<div in:fade class="chat__scroll-down">
<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}
{#if showFixedNewMessages}
2025-11-26 01:06:21 +00:00
<div class="relative z-popover flex justify-center">
<div transition:fly={{duration: 200}} class="fixed top-12 pt-sai">
<Button class="btn btn-primary btn-xs rounded-full" onclick={scrollToNewMessages}>
New Messages
</Button>
</div>
</div>
{/if}