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
6 changed files with 68 additions and 58 deletions
Showing only changes of commit 50199268f7 - Show all commits
+10 -18
View File
@@ -6,6 +6,7 @@
import {waitForThunkError, createRoom, editRoom, joinRoom} from "@welshman/app"
import StickerSmileSquare from "@assets/icons/sticker-smile-square.svg?dataurl"
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl"
import UploadMinimalistic from "@assets/icons/upload-minimalistic.svg?dataurl"
import {preventDefault} from "@lib/html"
import FieldInline from "@lib/components/FieldInline.svelte"
@@ -16,15 +17,7 @@
import ModalBody from "@lib/components/ModalBody.svelte"
import {pushToast} from "@app/util/toast"
import {uploadFile} from "@app/core/commands"
import {deriveHasLivekit} from "@app/core/state"
type RoomMode = "text" | "voice" | "both"
const getRoomMode = (room: RoomMeta): RoomMode => {
if (room.livekit && room.noText) return "voice"
if (room.livekit) return "both"
return "text"
}
import {deriveHasLivekit, getRoomType, RoomType} from "@app/core/state"
type Props = {
url: string
5
@@ -37,13 +30,13 @@
const {url, header, footer, onsubmit, initialValues = makeRoomMeta()}: Props = $props()
const values = $state(initialValues)
let roomMode = $state<RoomMode>(getRoomMode(initialValues))
let roomType = $state<RoomType>(getRoomType(initialValues))
const relayHasLivekit = deriveHasLivekit(url)
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)) ```
const submit = async () => {
const room = $state.snapshot(values)
if ((roomMode === "voice" || roomMode === "both") && !get(relayHasLivekit)) {
if (roomType === RoomType.Voice && !get(relayHasLivekit)) {
return pushToast({
theme: "error",
message: "This relay does not support voice rooms.",
1
@@ -71,8 +64,8 @@
}
if (get(relayHasLivekit)) {
room.livekit = roomMode === "both" || roomMode === "voice"
room.noText = roomMode === "voice"
room.livekit = roomType === RoomType.Voice
room.noText = false
}
const editMessage = await waitForThunkError(editRoom(url, room))
hodlbod marked this conversation as resolved Outdated
Outdated
Review

This should never run if we make the form change below.

This should never run if we make the form change below.
4
@@ -171,7 +164,7 @@
{#if imagePreview}
<ImageIcon src={imagePreview} alt="" class="rounded-lg" />
{:else}
<Icon icon={Hashtag} />
<Icon icon={roomType === RoomType.Voice ? Volume : Hashtag} />
{/if}
<input bind:value={values.name} class="grow" type="text" />
</label>
@@ -195,11 +188,10 @@
{#snippet input()}
<select
class="select select-bordered w-full"
bind:value={roomMode}
bind:value={roomType}
aria-label="Room type">
<option value="text">Text only</option>
<option value="both">Text and voice</option>
<option value="voice">Voice only</option>
<option value={RoomType.Text}>Text</option>
<option value={RoomType.Voice}>Voice</option>
</select>
{/snippet}
</FieldInline>
+20 -1
View File
@@ -1,8 +1,11 @@
<script lang="ts">
import Hashtag from "@assets/icons/hashtag.svg?dataurl"
import Volume from "@assets/icons/volume.svg?dataurl"
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import ImageIcon from "@lib/components/ImageIcon.svelte"
import {deriveRoom} from "@app/core/state"
import {currentVoiceSession} from "@app/voice"
interface Props {
h: string
@@ -14,9 +17,25 @@
const {url, h, size = 5, fallbackIcon = Hashtag}: Props = $props()
const room = deriveRoom(url, h)
const isVoiceRoom = $derived($room.livekit)
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 $room.picture}
{#if isVoiceRoom}
<div class="flex items-center gap-1 shrink-0">
<Icon
icon={typeIconSrc}
size={size + 1}
class={isVoiceRoomActive ? "text-primary -translate-x-0.5" : ""} />
{#if $room.picture}
<span class="text-base">/</span>
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
{/if}
</div>
{:else if $room.picture}
<ImageIcon src={$room.picture} {size} alt="" class="rounded-lg" />
{:else}
<Icon icon={fallbackIcon} {size} />
+6 -20
View File
@@ -38,7 +38,6 @@
import SpaceReports from "@app/components/SpaceReports.svelte"
import RoomCreate from "@app/components/RoomCreate.svelte"
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
import VoiceRoomItem from "@app/components/VoiceRoomItem.svelte"
import VoiceWidget from "@app/components/VoiceWidget.svelte"
import SocketStatusIndicator from "@app/components/SocketStatusIndicator.svelte"
import {
@@ -47,8 +46,6 @@
deriveSpaceMembers,
deriveUserRooms,
deriveOtherRooms,
deriveRoomsWithLivekit,
deriveRoomsNoText,
deriveOtherVoiceRooms,
userSpaceUrls,
hasNip29,
@@ -73,8 +70,6 @@
const calendarPath = makeSpacePath(url, "calendar")
const userRooms = deriveUserRooms(url)
const otherRooms = deriveOtherRooms(url)
const roomsWithLivekit = deriveRoomsWithLivekit(url)
const roomsNoText = deriveRoomsNoText(url)
const otherVoiceRooms = deriveOtherVoiceRooms(url)
const members = deriveSpaceMembers(url)
const userIsAdmin = deriveUserIsSpaceAdmin(url)
@@ -264,14 +259,7 @@
<SecondaryNavHeader>Your Rooms</SecondaryNavHeader>
{/if}
{#each $userRooms as h (h)}
{#if !$roomsNoText.has(h)}
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
{/if}
{#if $roomsWithLivekit.has(h)}
<div class="hidden md:block">
<VoiceRoomItem {url} {h} />
</div>
{/if}
<SpaceMenuRoomItem notify {replaceState} {url} {h} />
{/each}
{#if $otherRooms.length > 0}
<div class="h-2"></div>
@@ -293,13 +281,11 @@
<SpaceMenuRoomItem {replaceState} {url} {h} />
{/each}
{#if $otherVoiceRooms.length > 0}
<div class="hidden md:block">
<div class="h-2"></div>
<SecondaryNavHeader>Voice Rooms</SecondaryNavHeader>
{#each $otherVoiceRooms as h (h)}
<VoiceRoomItem {url} {h} />
{/each}
</div>
<div class="h-2"></div>
<SecondaryNavHeader>Voice Rooms</SecondaryNavHeader>
{#each $otherVoiceRooms as h (h)}
<SpaceMenuRoomItem {replaceState} {url} {h} />
{/each}
{/if}
{#if $canCreateRoom}
<SecondaryNavItem {replaceState} onclick={addRoom}>
4
+11 -1
View File
@@ -4,9 +4,10 @@
import Icon from "@lib/components/Icon.svelte"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import RoomNameWithImage from "@app/components/RoomNameWithImage.svelte"
import {deriveShouldNotify} from "@app/core/state"
import {deriveRoom, deriveShouldNotify, getRoomType, RoomType} from "@app/core/state"
import {notifications} from "@app/util/notifications"
import {makeRoomPath} from "@app/util/routes"
import {joinVoiceRoom, currentVoiceSession} from "@app/voice"
interface Props {
url: any
@@ -17,15 +18,24 @@
const {url, h, notify = false, replaceState = false}: Props = $props()
const room = deriveRoom(url, h)
const roomType = $derived(getRoomType($room))
const path = makeRoomPath(url, h)
const shouldNotifyForSpace = deriveShouldNotify(url)
const shouldNotifyForRoom = deriveShouldNotify(url, h)
const showDifferenceIcon = $derived($shouldNotifyForRoom !== $shouldNotifyForSpace)
const handleClick = () => {
if (roomType !== RoomType.Voice) return
if ($currentVoiceSession?.url === url && $currentVoiceSession?.h === h) return
void joinVoiceRoom(url, h)
}
</script>
<SecondaryNavItem
href={path}
{replaceState}
onclick={handleClick}
notification={notify ? $notifications.has(path) : false}>
<RoomNameWithImage {url} {h} />
{#if showDifferenceIcon}
+1 -3
View File
@@ -1,8 +1,6 @@
<script lang="ts">
import cx from "classnames"
import {loadProfile, displayProfileByPubkey} from "@welshman/app"
import Volume from "@assets/icons/volume.svg?dataurl"
import VolumeLoud from "@assets/icons/volume-loud.svg?dataurl"
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
import ProfileCircle from "@app/components/ProfileCircle.svelte"
import RoomImage from "@app/components/RoomImage.svelte"
8
@@ -62,7 +60,7 @@
{#if joinAbortController}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<RoomImage {url} {h} size={4} fallbackIcon={isActive ? VolumeLoud : Volume} />
<RoomImage {url} {h} size={4} />
{/if}
<RoomName {url} {h} />
</div>
2
+20 -15
View File
@@ -568,11 +568,19 @@ export const chatSearch = derived(throttled(800, chatsById), $chatsByPubkey => {
// Rooms
export enum RoomType {
Text = "text",
Voice = "voice",
}
export type Room = PublishedRoomMeta & {
id: string
url: string
}
export const getRoomType = (room: {livekit?: boolean}): RoomType =>
room.livekit ? RoomType.Voice : RoomType.Text
export const makeRoomId = (url: string, h: string) => `${url}'${h}`
export const splitRoomId = (id: string) => id.split("'")
4
@@ -664,7 +672,7 @@ export const displayRoom = (url: string, h: string) => getRoom(makeRoomId(url, h
export const roomComparator = (url: string) => (h: string) => displayRoom(url, h).toLowerCase()
export const deriveRoomsWithLivekit = (url: string) =>
export const deriveVoiceRooms = (url: string) =>
derived(roomsById, $roomsById => {
const set = new Set<string>()
for (const room of $roomsById.values()) {
@@ -687,20 +695,17 @@ export const deriveRoomsNoText = (url: string) =>
})
export const deriveOtherVoiceRooms = (url: string) =>
derived(
[deriveRoomsWithLivekit(url), deriveUserRooms(url)],
([$roomsWithLivekit, $userRooms]) => {
const rooms: string[] = []
derived([deriveVoiceRooms(url), deriveUserRooms(url)], ([$roomsWithLivekit, $userRooms]) => {
const rooms: string[] = []
for (const h of $roomsWithLivekit) {
if (!$userRooms.includes(h)) {
rooms.push(h)
}
for (const h of $roomsWithLivekit) {
if (!$userRooms.includes(h)) {
rooms.push(h)
}
}
return sortBy(roomComparator(url), uniq(rooms))
},
)
return sortBy(roomComparator(url), uniq(rooms))
})
// User space/room lists
@@ -792,12 +797,12 @@ export const deriveUserRooms = (url: string) =>
export const deriveOtherRooms = (url: string) =>
derived(
[deriveUserRooms(url), deriveRoomsNoText(url), roomsByUrl],
([$userRooms, $roomsNoText, $roomsByUrl]) => {
[deriveUserRooms(url), deriveVoiceRooms(url), roomsByUrl],
([$userRooms, voiceRooms, $roomsByUrl]) => {
const rooms: string[] = []
for (const {h} of $roomsByUrl.get(url) || []) {
if (!$userRooms.includes(h) && !$roomsNoText.has(h)) {
if (!$userRooms.includes(h) && !voiceRooms.has(h)) {
rooms.push(h)
}
}