ui stuff
This commit is contained in:
parent
0d79b42072
commit
d1a68c635c
5 changed files with 384 additions and 94 deletions
136
static/app.js
136
static/app.js
|
|
@ -16,13 +16,13 @@ const state = {
|
|||
localStream: null,
|
||||
rawStream: null,
|
||||
audioContext: null,
|
||||
denoiserNode: null,
|
||||
deepFilterCore: null,
|
||||
peerConnections: new Map(),
|
||||
muted: false,
|
||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
||||
},
|
||||
voicePresencePollId: null,
|
||||
chatWs: null,
|
||||
lastMessageId: null,
|
||||
};
|
||||
|
||||
const el = {
|
||||
|
|
@ -140,10 +140,47 @@ function formatDate(isoString) {
|
|||
return d.toLocaleDateString();
|
||||
}
|
||||
|
||||
const DEEPFILTERNET_LIB_PATH = "/static/vendor/deepfilternet3/index.esm.js";
|
||||
const NSNET2_COMPAT_SUPPRESSION = 56;
|
||||
let audioCtx = null;
|
||||
function playSound(type) {
|
||||
try {
|
||||
if (!audioCtx) audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
if (audioCtx.state === 'suspended') audioCtx.resume();
|
||||
|
||||
let deepFilterLibPromise = null;
|
||||
const sounds = {
|
||||
message: { freq: 880, type: 'sine', duration: 0.1, volume: 0.1 },
|
||||
dm: { freq: [660, 880], type: 'sine', duration: 0.15, volume: 0.1 },
|
||||
join: { freq: [440, 880], type: 'sine', duration: 0.2, volume: 0.1 },
|
||||
leave: { freq: [880, 440], type: 'sine', duration: 0.2, volume: 0.1 },
|
||||
'peer-join': { freq: [660, 990], type: 'sine', duration: 0.15, volume: 0.05 },
|
||||
'peer-leave': { freq: [990, 660], type: 'sine', duration: 0.15, volume: 0.05 },
|
||||
};
|
||||
|
||||
const s = sounds[type];
|
||||
if (!s) return;
|
||||
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
|
||||
osc.type = s.type;
|
||||
if (Array.isArray(s.freq)) {
|
||||
osc.frequency.setValueAtTime(s.freq[0], audioCtx.currentTime);
|
||||
osc.frequency.exponentialRampToValueAtTime(s.freq[1], audioCtx.currentTime + s.duration);
|
||||
} else {
|
||||
osc.frequency.setValueAtTime(s.freq, audioCtx.currentTime);
|
||||
}
|
||||
|
||||
gain.gain.setValueAtTime(s.volume, audioCtx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + s.duration);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(audioCtx.destination);
|
||||
|
||||
osc.start();
|
||||
osc.stop(audioCtx.currentTime + s.duration);
|
||||
} catch (err) {
|
||||
console.warn("playSound failed", err);
|
||||
}
|
||||
}
|
||||
const LAST_GUILD_STORAGE_KEY = "chattz:lastGuildId";
|
||||
|
||||
// --- Renderers ---
|
||||
|
|
@ -183,7 +220,7 @@ function renderChannels() {
|
|||
for (const channel of state.channels) {
|
||||
const row = document.createElement("button");
|
||||
row.className = `channel-row ${(channel.kind === 'text' && state.selectedTextChannelId === channel.id) ||
|
||||
(channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : ""
|
||||
(channel.kind === 'voice' && state.selectedVoiceChannelId === channel.id) ? "active" : ""
|
||||
}`;
|
||||
|
||||
const iconName = channel.kind === 'text' ? 'hash' : 'volume-2';
|
||||
|
|
@ -291,6 +328,14 @@ function renderMessages(messages) {
|
|||
lastTime = mDate;
|
||||
}
|
||||
|
||||
if (messages.length > 0) {
|
||||
const latest = messages[0];
|
||||
if (state.lastMessageId && latest.id !== state.lastMessageId && latest.author_user_id !== state.me?.id) {
|
||||
playSound(state.selectedDmUserId ? 'dm' : 'message');
|
||||
}
|
||||
state.lastMessageId = latest.id;
|
||||
}
|
||||
|
||||
el.messageList.scrollTop = el.messageList.scrollHeight;
|
||||
}
|
||||
|
||||
|
|
@ -422,6 +467,39 @@ function startVoicePresencePolling() {
|
|||
}, 3000);
|
||||
}
|
||||
|
||||
function initChatWs() {
|
||||
if (state.chatWs) state.chatWs.close();
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const wsUrl = `${protocol}//${location.host}/ws`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
state.chatWs = ws;
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
const msg = JSON.parse(event.data);
|
||||
if (msg.type === "message_created") {
|
||||
if (state.selectedTextChannelId === msg.channel_id) {
|
||||
// Optimization: we could just push the message, but refresh for now
|
||||
api(`/channels/${state.selectedTextChannelId}/messages?limit=100`).then(renderMessages);
|
||||
} else {
|
||||
// Even if not active, play sound
|
||||
playSound('message');
|
||||
}
|
||||
} else if (msg.type === "dm_created") {
|
||||
if (state.selectedDmUserId === msg.other_user_id) {
|
||||
api(`/dms/${state.selectedDmUserId}/messages?limit=100`).then(renderMessages);
|
||||
} else {
|
||||
playSound('dm');
|
||||
loadDMConversations().then(renderDMs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log("Chat WS closed, reconnecting...");
|
||||
setTimeout(initChatWs, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
// --- Voice ---
|
||||
|
||||
function getVoiceWsUrl(channelId) {
|
||||
|
|
@ -434,40 +512,7 @@ function shouldInitiateOffer(peerId) {
|
|||
return state.me.id > peerId;
|
||||
}
|
||||
|
||||
async function loadDeepFilterLib() {
|
||||
if (!deepFilterLibPromise) {
|
||||
deepFilterLibPromise = import(DEEPFILTERNET_LIB_PATH);
|
||||
}
|
||||
return deepFilterLibPromise;
|
||||
}
|
||||
|
||||
async function buildDenoiserNode(audioContext) {
|
||||
if (!audioContext.audioWorklet) {
|
||||
throw new Error("AudioWorklet is not supported in this browser");
|
||||
}
|
||||
|
||||
const df = await loadDeepFilterLib();
|
||||
const core = new df.DeepFilterNet3Core({
|
||||
sampleRate: 48000,
|
||||
noiseReductionLevel: NSNET2_COMPAT_SUPPRESSION,
|
||||
assetConfig: {
|
||||
cdnUrl: "/static/vendor/deepfilternet3",
|
||||
},
|
||||
});
|
||||
await core.initialize();
|
||||
const workletNode = await core.createAudioWorkletNode(audioContext);
|
||||
state.voice.deepFilterCore = core;
|
||||
return workletNode;
|
||||
}
|
||||
|
||||
function stopAndClearAudioPipeline() {
|
||||
if (state.voice.deepFilterCore) {
|
||||
state.voice.deepFilterCore.destroy();
|
||||
state.voice.deepFilterCore = null;
|
||||
}
|
||||
if (state.voice.denoiserNode && typeof state.voice.denoiserNode.destroy === "function") {
|
||||
state.voice.denoiserNode.destroy();
|
||||
}
|
||||
if (state.voice.localStream) {
|
||||
for (const track of state.voice.localStream.getTracks()) track.stop();
|
||||
}
|
||||
|
|
@ -480,8 +525,6 @@ function stopAndClearAudioPipeline() {
|
|||
state.voice.localStream = null;
|
||||
state.voice.rawStream = null;
|
||||
state.voice.audioContext = null;
|
||||
state.voice.denoiserNode = null;
|
||||
state.voice.deepFilterCore = null;
|
||||
}
|
||||
|
||||
async function buildAudioPipeline(rawStream) {
|
||||
|
|
@ -492,14 +535,8 @@ async function buildAudioPipeline(rawStream) {
|
|||
|
||||
let head = source;
|
||||
|
||||
const denoiserNode = await buildDenoiserNode(audioContext);
|
||||
if (denoiserNode) {
|
||||
head.connect(denoiserNode);
|
||||
head = denoiserNode;
|
||||
}
|
||||
head.connect(destination);
|
||||
|
||||
state.voice.denoiserNode = denoiserNode;
|
||||
return destination.stream;
|
||||
}
|
||||
|
||||
|
|
@ -621,6 +658,7 @@ async function joinVoice() {
|
|||
const channel = state.channels.find(c => c.id === state.selectedVoiceChannelId);
|
||||
el.vcChannelName.textContent = channel ? channel.name : "Voice";
|
||||
el.voiceConnection.classList.remove("hidden");
|
||||
playSound('join');
|
||||
refreshVoicePresence().catch(() => { });
|
||||
};
|
||||
|
||||
|
|
@ -631,8 +669,10 @@ async function joinVoice() {
|
|||
if (shouldInitiateOffer(peer.user_id)) await sendOffer(peer.user_id);
|
||||
}
|
||||
} else if (msg.type === "peer_joined") {
|
||||
playSound('peer-join');
|
||||
if (shouldInitiateOffer(msg.user_id)) await sendOffer(msg.user_id);
|
||||
} else if (msg.type === "peer_left") {
|
||||
playSound('peer-leave');
|
||||
const pc = state.voice.peerConnections.get(msg.user_id);
|
||||
if (pc) {
|
||||
pc.close();
|
||||
|
|
@ -652,6 +692,7 @@ async function joinVoice() {
|
|||
stopAndClearAudioPipeline();
|
||||
state.voice.joinedChannelId = null;
|
||||
state.voice.ws = null;
|
||||
playSound('leave');
|
||||
refreshVoicePresence().catch(() => { });
|
||||
};
|
||||
}
|
||||
|
|
@ -893,6 +934,7 @@ async function init() {
|
|||
updateHeaderLabels();
|
||||
}
|
||||
|
||||
initChatWs();
|
||||
startVoicePresencePolling();
|
||||
lucide.createIcons();
|
||||
} catch (err) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue