feature/23-voice-room/poc #93
@@ -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
|
||||
@@ -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
|
||||
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.",
|
||||
@@ -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
hodlbod
commented
This should never run if we make the form change below. This should never run if we make the form change below.
|
||||
@@ -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>
|
||||
|
||||
@@ -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
hodlbod
commented
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?
mplorentz
commented
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.
mplorentz
commented
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.
hodlbod
commented
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} />
|
||||
|
||||
@@ -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,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,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"
|
||||
@@ -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>
|
||||
|
||||
@@ -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("'")
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
[nit]
Stylistically I normally do something like: