9df8cee501
Adopts the rewritten welshman API: the removed @welshman/util helpers (Profile/List/Room/Handler/Encryptable) are now Reader/Builder classes in @welshman/domain, and @welshman/app dropped its global singletons for an App instance + app.use(Plugin) registry. - src/app/welshman.ts is now the app bootstrap + session-state module (one shared App instance, multi-account sessions/login, app-wide reactive views) rather than a compat shim re-exporting the old globals. - Rewrote ~100 callers to use app.use(Plugin) directly (thunks, profiles, relays, rooms, zaps, tags, wot, feeds, sync); thunk helpers are now thunk methods. - Added @welshman/domain dependency. - Resolved residual gaps (storage hydration via plugin.onItem/wrapManager/Plaintext, relay-list mutators, search-relay list, outbox #d filter). Best-effort: no toolchain/linking available, so this is not build- or type-checked. Remaining judgment calls are flagged with TODO(welshman-migration). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsMjvv7krpZeHK1Njeneru
119 lines
3.7 KiB
Svelte
119 lines
3.7 KiB
Svelte
<script lang="ts">
|
|
import cx from "classnames"
|
|
import {goto} from "$app/navigation"
|
|
import {Profiles} from "@welshman/app"
|
|
import {app} from "@app/welshman"
|
|
import SecondaryNavItem from "@lib/components/SecondaryNavItem.svelte"
|
|
import ProfileCircle from "@app/components/ProfileCircle.svelte"
|
|
import ProfileCircles from "@app/components/ProfileCircles.svelte"
|
|
import RoomImage from "@app/components/RoomImage.svelte"
|
|
import RoomName from "@app/components/RoomName.svelte"
|
|
import {makeRoomPath} from "@app/routes"
|
|
import {pushModal} from "@app/modal"
|
|
import VoiceRoomJoinDialog from "@app/components/VoiceRoomJoinDialog.svelte"
|
|
import VoiceParticipantMediaBadges from "@app/components/VoiceParticipantMediaBadges.svelte"
|
|
import {makeRoomId} from "@app/groups"
|
|
import {
|
|
VoiceState,
|
|
currentVoiceRoom,
|
|
isParticipantSpeaking,
|
|
mediaStateByIdentity,
|
|
participantKey,
|
|
voiceState,
|
|
type VoiceParticipant,
|
|
} from "@app/call/stores"
|
|
import {
|
|
cancelJoinVoiceRoom,
|
|
deriveVoiceParticipants,
|
|
loadVoiceParticipants,
|
|
} from "@app/call/voice"
|
|
|
|
interface Props {
|
|
url: string
|
|
h: string
|
|
replaceState?: boolean
|
|
notification?: boolean
|
|
}
|
|
|
|
const {url, h, replaceState = false, notification = false}: Props = $props()
|
|
|
|
const participants = deriveVoiceParticipants(url, h)
|
|
const participantPubkeys = $derived($participants.flatMap(p => (p.pubkey ? [p.pubkey] : [])))
|
|
const isActive = $derived(
|
|
$voiceState === VoiceState.Connected && $currentVoiceRoom?.id === makeRoomId(url, h),
|
|
)
|
|
const isJoining = $derived(
|
|
$voiceState === VoiceState.Joining && $currentVoiceRoom?.id === makeRoomId(url, h),
|
|
)
|
|
|
|
const handleClick = async (e: MouseEvent) => {
|
|
if (isActive) return
|
|
|
|
if (isJoining) {
|
|
e.preventDefault()
|
|
cancelJoinVoiceRoom()
|
|
return
|
|
}
|
|
|
|
e.preventDefault()
|
|
await goto(makeRoomPath(url, h), {replaceState})
|
|
pushModal(VoiceRoomJoinDialog, {url, h})
|
|
}
|
|
|
|
$effect(() => {
|
|
void loadVoiceParticipants(url, h)
|
|
})
|
|
|
|
$effect(() => {
|
|
for (const p of $participants) {
|
|
if (p.pubkey) app.use(Profiles).load(p.pubkey)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<SecondaryNavItem
|
|
href={makeRoomPath(url, h)}
|
|
{replaceState}
|
|
{notification}
|
|
onclick={handleClick}
|
|
class={cx("items-start!", isActive && "bg-base-100! text-base-content!")}>
|
|
<div class="flex w-full min-w-0 flex-col gap-2">
|
|
<div class="flex gap-2 items-center">
|
|
{#if isJoining}
|
|
<span class="loading loading-spinner loading-sm"></span>
|
|
{:else}
|
|
<RoomImage {url} {h} size={4} />
|
|
{/if}
|
|
<RoomName {url} {h} />
|
|
</div>
|
|
{#if participantPubkeys.length > 0}
|
|
{#if isActive}
|
|
{#each $participants as p (participantKey(p as VoiceParticipant))}
|
|
{@const media = $mediaStateByIdentity(p.identity)}
|
|
<div class="flex items-center gap-2 ml-6">
|
|
<div
|
|
class={cx(
|
|
"inline-flex shrink-0 items-center justify-center rounded-full transition-shadow",
|
|
$isParticipantSpeaking(p) && "ring-2 ring-success",
|
|
)}>
|
|
<ProfileCircle pubkey={p.pubkey} size={5} class="h-5 w-5" />
|
|
</div>
|
|
<span class="ellipsize min-w-0 flex-1 text-xs opacity-70">
|
|
{p.pubkey ? app.use(Profiles).display(p.pubkey).get() : "Unknown"}
|
|
</span>
|
|
<VoiceParticipantMediaBadges
|
|
muted={media.muted}
|
|
cameraOn={media.cameraOn}
|
|
size={3}
|
|
class="shrink-0" />
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="ml-6">
|
|
<ProfileCircles pubkeys={participantPubkeys} size={5} limit={3} />
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</SecondaryNavItem>
|