add more ux and ui patches with extra login helper

This commit is contained in:
JCEEE
2026-08-27 14:13:14 +01:00
parent 84b3cd48f4
commit 9f335db7b2
16 changed files with 643 additions and 150 deletions
+37 -2
View File
@@ -40,6 +40,7 @@ class ChatStore {
private initPromise: Promise<void> | null = null;
private lastSeenAt = 0;
private typingTimers = new Map<string, ReturnType<typeof setTimeout>>();
private audioCtx: AudioContext | null = null;
typingNames = $derived.by(() => {
const names: string[] = [];
@@ -156,8 +157,9 @@ class ChatStore {
}
// Stop showing their typing indicator once the message lands.
this.clearTyping(record.authorId);
if (record.authorId !== this.actorId && !this.open) {
this.unread++;
if (record.authorId !== this.actorId) {
if (!this.open) this.unread++;
this.playDunk();
}
}
@@ -184,6 +186,39 @@ class ChatStore {
if (this.typing[actorId]) this.removeTyping(actorId);
}
// Short, low-volume "dunk" blip played when a message from someone else is
// broadcast in. Synthesised with the Web Audio API so there's no asset to
// ship. Browsers require a user gesture before audio can start, but chat
// messages only arrive after the user has interacted, so the context
// resumes fine. Failures are silently ignored.
private playDunk() {
if (typeof window === 'undefined' || typeof AudioContext === 'undefined') return;
try {
const Ctor = window.AudioContext || (window as any).webkitAudioContext;
if (!Ctor) return;
if (!this.audioCtx) this.audioCtx = new Ctor();
const ctx = this.audioCtx;
if (ctx.state === 'suspended') ctx.resume();
const now = ctx.currentTime;
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
// Quick downward pitch sweep gives the soft "dun-nk" thud.
osc.frequency.setValueAtTime(190, now);
osc.frequency.exponentialRampToValueAtTime(70, now + 0.18);
// Low volume with a fast attack/decay envelope.
gain.gain.setValueAtTime(0.0001, now);
gain.gain.exponentialRampToValueAtTime(0.12, now + 0.01);
gain.gain.exponentialRampToValueAtTime(0.0001, now + 0.22);
osc.connect(gain).connect(ctx.destination);
osc.start(now);
osc.stop(now + 0.24);
} catch {
/* audio is best-effort */
}
}
private removeTyping(actorId: string) {
if (!this.typing[actorId]) return;
const next = { ...this.typing };