Add channel and permission management, sounds, themes and the public server list

- Channels: create, edit, delete, link and move (drag and drop for channels and
  people), right-click on free space in the channel list
- Permission editor with inherited rules, allow/deny per permission, groups and
  members; menus grey out actions the server does not allow
- Notification sounds for connection, channel, chat, mute and push-to-talk events;
  custom files and TeamSpeak 3 style sound packs (settings.ini), stored in IndexedDB
- Color schemes: Dark, Light, Midnight, Frost and Windows 95
- Public server list with live UDP ping, fetched through Chromium's network stack
- Kick, ban and connection-lost card with Reconnect
- Right-click menus for text fields, links, images and message authors
- Formatting toolbar in the message box, shared with the description editor
- Resizable channel list, menu headers, registration from the self menu only
- Fix: client-side pacing now mirrors Murmur's whole-second leaky bucket exactly,
  so quick actions are no longer dropped silently

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 00:54:03 +02:00
co-authored by Claude Opus 5.5
parent aa679dddcc
commit 9a3aeb29ad
53 changed files with 2684 additions and 170 deletions
+11 -2
View File
@@ -3,11 +3,20 @@
import YouTubeEmbed from './YouTubeEmbed.svelte';
import LinkPreview from './LinkPreview.svelte';
import { store } from '../lib/settings.svelte.ts';
import { session } from '../lib/session.svelte.ts';
import { menu } from '../lib/menu.svelte.ts';
import { userMenu } from '../lib/actions.ts';
import type { ChatEntry } from '../lib/session.svelte.ts';
let { entry, grouped }: { entry: ChatEntry; grouped: boolean } = $props();
const time = $derived(new Date(entry.time));
// Right-click on the author opens the user menu while they are still online
function authorMenu(e: MouseEvent) {
const u = entry.authorSession != null ? session.user(entry.authorSession) : undefined;
if (u) menu.show(e, userMenu(u));
}
const hhmm = $derived(time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }));
</script>
@@ -22,13 +31,13 @@
{#if grouped}
<span class="time hover">{hhmm}</span>
{:else}
<Avatar name={entry.author} size={38} />
<span role="presentation" oncontextmenu={authorMenu}><Avatar name={entry.author} size={38} /></span>
{/if}
</div>
<div class="content">
{#if !grouped}
<header>
<strong class:self={entry.self}>{entry.author}</strong>
<strong class:self={entry.self} oncontextmenu={authorMenu}>{entry.author}</strong>
<time datetime={time.toISOString()}>{time.toLocaleString([], { dateStyle: 'short', timeStyle: 'short' })}</time>
</header>
{/if}