Initial commit: mumh5, a modern Mumble client
Electron desktop app with a Svelte 5 interface for any Mumble server. - Mumble protocol core: TLS, handshake, channels, users, text, plugin data, client-side pacing of Murmur's rate limits - Voice: WebCodecs Opus over the TCP tunnel, voice activity, push to talk, always-on, devices, per-user volume and local mute - Several servers at once, voice on one; server rail with icons and ordering - Chat: channels, direct messages, side chat, file sharing through f0ckm, inline images without it, click-to-play YouTube - Profiles with rich descriptions, registration, rename, nicknames, connection information and moderation menus - Identity wizard, multiple identities, PKCS#12 import/export, desktop Mumble certificate import, certificate pinning and viewer - Tray icon with voice state - Unit, server and end-to-end tests against a real Murmur Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
import Avatar from './Avatar.svelte';
|
||||
import Icon from './Icon.svelte';
|
||||
import { session } from '../lib/session.svelte.ts';
|
||||
import { menu } from '../lib/menu.svelte.ts';
|
||||
import { userMenu } from '../lib/actions.ts';
|
||||
import { voice } from '../lib/audio/voice.svelte.ts';
|
||||
import { store } from '../lib/settings.svelte.ts';
|
||||
|
||||
let { onnavigate }: { onnavigate: () => void } = $props();
|
||||
|
||||
const here = $derived(session.self ? session.usersIn(session.self.channelId) : []);
|
||||
const elsewhere = $derived(session.allUsers().filter(u => u.channelId !== session.self?.channelId));
|
||||
</script>
|
||||
|
||||
<aside class="members" aria-label="Members">
|
||||
{#snippet group(title: string, users: typeof here)}
|
||||
{#if users.length}
|
||||
<h3>{title} ({users.length})</h3>
|
||||
<ul>
|
||||
{#each users as u (u.session)}
|
||||
{@const isSelf = u.session === session.self?.session}
|
||||
<li>
|
||||
<button onclick={() => session.showUser(u.session)} title={isSelf ? 'You' : `${u.name} (double-click to message)`}
|
||||
ondblclick={() => { if (!isSelf) { session.selectView(`dm:${u.session}`); onnavigate(); } }}
|
||||
oncontextmenu={e => menu.show(e, userMenu(u))}>
|
||||
<Avatar name={u.name} size={30} speaking={voice.talking[u.session] || (isSelf && voice.transmitting)} />
|
||||
<span class="who">
|
||||
<span class="name">{store.displayName(u)}</span>
|
||||
<span class="sub">{u.userId != null ? 'Registered' : 'Guest'}{title !== 'In channel' ? ` in ${session.channel(u.channelId)?.name || 'Root'}` : ''}</span>
|
||||
</span>
|
||||
{#if u.selfDeaf || u.deaf}<Icon name="headphones-off" size={14} />{:else if u.selfMute || u.mute}<Icon name="mic-off" size={14} />{/if}
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/snippet}
|
||||
{@render group('In channel', here)}
|
||||
{@render group('Elsewhere', elsewhere)}
|
||||
</aside>
|
||||
|
||||
<style>
|
||||
.members { flex: 1; min-height: 0; overflow-y: auto; padding: 8px 6px; }
|
||||
h3 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--text-faint); margin: 14px 8px 6px; }
|
||||
ul { list-style: none; margin: 0; padding: 0; }
|
||||
button { display: flex; align-items: center; gap: 10px; width: 100%; padding: 5px 8px; text-align: left; color: var(--text-dim); }
|
||||
button:hover { background: var(--bg-hover); color: var(--text); }
|
||||
.who { flex: 1; min-width: 0; display: flex; flex-direction: column; line-height: 1.2; }
|
||||
.name { color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.sub { font-size: 11px; color: var(--text-faint); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user