feature/23-voice-room/poc #93

Merged
hodlbod merged 68 commits from feature/23-voice-room/poc into dev 2026-03-16 20:38:06 +00:00
7 changed files with 20 additions and 45 deletions
Showing only changes of commit 33b9edb8b5 - Show all commits
+2 -2
View File
@@ -4,7 +4,7 @@
FROM node:20-bookworm AS builder
RUN apt-get update && apt-get install -y --no-install-recommends curl
RUN apt-get update && apt-get install -y --no-install-recommends curl
RUN npm install -g pnpm@latest
@@ -29,4 +29,4 @@ WORKDIR /app
# Copy only the built output - no source, no .env, no dev deps
COPY --from=builder /app/build ./build
CMD ["npx", "serve", "build"]
CMD ["npx", "serve", "-s", "build"]
+4 -8
View File
@@ -1,14 +1,12 @@
<script lang="ts">
import type {Readable} from "svelte/store"
import {readable} from "svelte/store"
import cx from "classnames"
import {ifLet, removeUndefined} from "@welshman/lib"
import {removeUndefined} from "@welshman/lib"
import {deriveProfile} from "@welshman/app"
import UserRounded from "@assets/icons/user-rounded.svg?dataurl"
import ImageIcon from "@lib/components/ImageIcon.svelte"
type Props = {
pubkey?: string
pubkey: string
class?: string
size?: number
url?: string
@@ -16,13 +14,11 @@
const {pubkey, url, size = 7, ...props}: Props = $props()
const readableProfile = ifLet(pubkey, pk => deriveProfile(pk, removeUndefined([url])))
const emptyProfile = readable(undefined)
const profile: Readable<{picture?: string} | undefined> = readableProfile ?? emptyProfile
const profile = deriveProfile(pubkey, removeUndefined([url]))
</script>
<ImageIcon
{size}
Outdated
Review

deriveProfile is designed to accept undefined to avoid this kind of mess

`deriveProfile` is designed to accept `undefined` to avoid this kind of mess
alt=""
class={cx(props.class, "rounded-full")}
src={$profile?.picture ?? UserRounded} />
src={$profile?.picture || UserRounded} />
+4 -7
View File
@@ -1,5 +1,4 @@
<script lang="ts">
import {get} from "svelte/store"
import type {Snippet} from "svelte"
import type {RoomMeta} from "@welshman/util"
import {makeRoomMeta} from "@welshman/util"
5
@@ -30,19 +29,20 @@
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
const values = $state(initialValues)
let roomType = $state<RoomType>(getRoomType(initialValues))
const relayHasLivekit = deriveHasLivekit(url)
const submit = async () => {
const room = $state.snapshot(values)
hodlbod marked this conversation as resolved Outdated
Outdated
Review

[nit]

Stylistically I normally do something like:

return uniqBy(nth(0), append(["livekit"], tags))
[nit] Stylistically I normally do something like: ``` return uniqBy(nth(0), append(["livekit"], tags)) ```
if (roomType === RoomType.Voice && !get(relayHasLivekit)) {
if (roomType === RoomType.Voice && !$relayHasLivekit) {
return pushToast({
theme: "error",
message: "This relay does not support voice rooms.",
})
}
room.livekit = roomType === RoomType.Voice
if (imageFile) {
const {error, result} = await uploadFile(imageFile, {
maxWidth: 256,
1
@@ -63,10 +63,6 @@
return pushToast({theme: "error", message: createMessage})
}
if (get(relayHasLivekit)) {
room.livekit = roomType === RoomType.Voice
room.noText = false
}
const editMessage = await waitForThunkError(editRoom(url, room))
if (editMessage) {
5
@@ -95,6 +91,7 @@
let loading = $state(false)
let imageFile = $state<File | undefined>()
let imagePreview = $state(initialValues.picture)
let roomType = $state(getRoomType(initialValues))
const handleImageUpload = async (event: Event) => {
const file = (event.target as HTMLInputElement).files?.[0]
+1 -2
View File
@@ -21,14 +21,13 @@
const isVoiceRoomActive = $derived(
$currentVoiceSession?.url === url && $currentVoiceSession?.h === h,
)
hodlbod marked this conversation as resolved Outdated
Outdated
Review

Now that we're showing the widget in the room, I think we can probably get rid of this change, what do you think?

Now that we're showing the widget in the room, I think we can probably get rid of this change, what do you think?
Outdated
Review

I'm confused - did you want to show the VoiceWidget only on the room page? Right now I have it showing in the SpaceMenu on desktop and both in SpaceMenu and on the room page on mobile. It seems like a better use of space on desktop to have it down in the corner than to stretch it across the whole width of the chat room.

I'm confused - did you want to show the VoiceWidget *only* on the room page? Right now I have it showing in the SpaceMenu on desktop and both in SpaceMenu and on the room page on mobile. It seems like a better use of space on desktop to have it down in the corner than to stretch it across the whole width of the chat room.
Outdated
Review

I guess that's orthogonal to the question of whether we want a different icon for voice rooms once you have joined them. I think it's a nice affordance if you are looking a the sidebar to be able to tell which room you are in without having to read the room name out of the voice widget.

I guess that's orthogonal to the question of whether we want a different icon for voice rooms once you have joined them. I think it's a nice affordance if you are looking a the sidebar to be able to tell which room you are in without having to read the room name out of the voice widget.
Outdated
Review

Yeah, I just mean we should just show the room's actual icon at the top of the page regardless of whether the room is active (the widget at the bottom will be indication enough)

Yeah, I just mean we should just show the room's actual icon at the top of the page regardless of whether the room is active (the widget at the bottom will be indication enough)
const typeIconSrc = $derived(isVoiceRoomActive ? VolumeLoud : Volume)
</script>
{#if isVoiceRoom}
<div class="flex shrink-0 items-center gap-1.5">
<Icon
icon={typeIconSrc}
size={size + 1}
icon={isVoiceRoomActive ? VolumeLoud : Volume}
class={isVoiceRoomActive ? "text-primary -translate-x-0.5" : ""} />
{#if $room.picture}
<span class="text-base">/</span>
+5 -13
View File
@@ -12,13 +12,12 @@
interface Props {
back?: () => unknown
icon?: Snippet
title?: Snippet
action?: Snippet
[key: string]: any
}
const {back = () => goto(makeSpacePath(url)), icon, title, action, ...props}: Props = $props()
const {back = () => goto(makeSpacePath(url)), title, action, ...props}: Props = $props()
const url = decodeRelay($page.params.relay!)
</script>
@@ -28,17 +27,10 @@
<Button onclick={back} class="place-self-start pr-3 md:hidden">
<Icon icon={ArrowLeft} size={7} />
</Button>
<div class="flex min-w-0 flex-grow items-center justify-between gap-4">
<div class="flex min-w-0 flex-col">
<div class="flex min-w-0 items-center gap-2">
{#if icon}
<div class="shrink-0">{@render icon?.()}</div>
<div class="ellipsize min-w-0 whitespace-nowrap">{@render title?.()}</div>
{:else}
<div class="ellipsize min-w-0 flex items-center gap-2 whitespace-nowrap">
{@render title?.()}
</div>
{/if}
<div class="ellipsize whitespace-nowrap flex flex-grow items-center justify-between gap-4">
<div class="flex flex-col">
<div class="flex gap-2 items-center">
{@render title?.()}
</div>
<div class="text-xs text-primary md:hidden">
{displayRelayUrl(url)}
+2 -1
View File
4
@@ -296,7 +296,8 @@
<div class="h-5 flex-shrink-0"></div>
</div>
</SecondaryNavSection>
<div class="flex flex-shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+3.5rem)] z-nav">
<div
class="flex flex-shrink-0 flex-col gap-2 p-2 pt-0 -mt-4 pb-[calc(var(--saib)+3rem)] sm:pb-2 z-nav">
<VoiceWidget />
<Button class="btn btn-neutral btn-sm h-10" onclick={showDetail}>
<SocketStatusIndicator {url} />
+2 -12
View File
@@ -125,6 +125,7 @@ import type {
RelayProfile,
PublishedList,
PublishedRoomMeta,
RoomMeta,
List,
Filter,
} from "@welshman/util"
@@ -578,7 +579,7 @@ export type Room = PublishedRoomMeta & {
url: string
}
export const getRoomType = (room: {livekit?: boolean}): RoomType =>
export const getRoomType = (room: RoomMeta): RoomType =>
room.livekit ? RoomType.Voice : RoomType.Text
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
4
@@ -683,17 +684,6 @@ export const deriveVoiceRooms = (url: string) =>
return set
})
export const deriveRoomsNoText = (url: string) =>
derived(roomsById, $roomsById => {
const set = new Set<string>()
for (const room of $roomsById.values()) {
if (room.url === url && room.noText) {
set.add(room.h)
}
}
return set
})
export const deriveOtherVoiceRooms = (url: string) =>
derived([deriveVoiceRooms(url), deriveUserRooms(url)], ([$roomsWithLivekit, $userRooms]) => {
const rooms: string[] = []