feature/23-voice-room/poc #93
@@ -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"]
|
||||
|
||||
@@ -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}
|
||||
|
|
||||
alt=""
|
||||
class={cx(props.class, "rounded-full")}
|
||||
src={$profile?.picture ?? UserRounded} />
|
||||
src={$profile?.picture || UserRounded} />
|
||||
|
||||
@@ -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"
|
||||
@@ -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
hodlbod
commented
[nit] Stylistically I normally do something like: [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,
|
||||
@@ -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) {
|
||||
@@ -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]
|
||||
|
||||
@@ -21,14 +21,13 @@
|
||||
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 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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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}`
|
||||
@@ -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[] = []
|
||||
|
||||
deriveProfileis designed to acceptundefinedto avoid this kind of mess