diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 1dd325b..63c9a7f 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -21,7 +21,10 @@ use ringbuf::{ }; use tauri::State; -use crate::{commands::config::ConfigState, crypto::SessionCipher}; +use crate::{ + commands::config::{BackendConfig, ConfigState}, + crypto::SessionCipher, +}; const TARGET_SAMPLE_RATE: u32 = 48_000; const PACKET_SAMPLES: usize = 960; @@ -57,6 +60,8 @@ pub struct VoiceSession { pub producer_in: AudioProducer, pub consumer_out: AudioConsumer, + + pub backend_config: Arc>, } fn resolve_input_device(device_name: &Option) -> Result { @@ -72,9 +77,9 @@ fn resolve_input_device(device_name: &Option) -> Result host.default_input_device().ok_or_else(|| { - format!("No default input device") - }), + None => host + .default_input_device() + .ok_or_else(|| format!("No default input device")), } } @@ -91,9 +96,9 @@ fn resolve_output_device(device_name: &Option) -> Result host.default_output_device().ok_or_else(|| { - format!("No default output device") - }), + None => host + .default_output_device() + .ok_or_else(|| format!("No default output device")), } } @@ -561,6 +566,11 @@ pub fn connect_to_vc( current_output_device: config.output_device_name.clone(), producer_in: Arc::clone(&shared_producer_in), consumer_out: Arc::clone(&shared_consumer_out), + + backend_config: Arc::new(Mutex::new(BackendConfig { + is_muted: false, + is_deaf: false, + })), }; session.setup_input(config.input_device_name.clone(), Arc::clone(&state_inner))?; @@ -571,6 +581,7 @@ pub fn connect_to_vc( let input_socket = socket.clone(); let shutdown = shutdown.clone(); let cipher = cipher.clone(); + let backend_config = session.backend_config.clone(); thread::spawn(move || { let mut sequence = 0u32; @@ -581,6 +592,22 @@ pub fn connect_to_vc( let mut noise_floor: f32 = 0.0; while !shutdown.load(Ordering::Relaxed) { + let (is_muted, is_deaf) = { + let cfg = backend_config.lock().unwrap(); + (cfg.is_muted, cfg.is_deaf) + }; + + if is_muted || is_deaf { + // Clear audio recorded while muted to avoid stale transmission on unmute + let available = consumer_in.occupied_len(); + if available > 0 { + consumer_in.skip(available); + } + + thread::sleep(Duration::from_millis(50)); + continue; + } + if consumer_in.occupied_len() >= PACKET_SAMPLES { let _ = consumer_in.pop_slice(&mut frame_buf); @@ -654,6 +681,7 @@ pub fn connect_to_vc( let socket = socket.clone(); let shutdown = shutdown.clone(); let cipher = cipher.clone(); + let backend_config = session.backend_config.clone(); thread::spawn(move || { let mut packets: BTreeMap> = BTreeMap::new(); @@ -666,6 +694,16 @@ pub fn connect_to_vc( let mut loop_count: u64 = 0; while !shutdown.load(Ordering::Relaxed) { + let is_deaf = { backend_config.lock().unwrap().is_deaf }; + + if is_deaf { + packets.clear(); + is_prebuffering = true; + + std::thread::sleep(Duration::from_secs(1)); + continue; + } + loop_count += 1; if let Ok(len) = socket.recv(&mut udp_buffer) { recv_count += 1; diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 32af22b..c8d672f 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -102,3 +102,22 @@ pub fn get_config(app: AppHandle, state: State) -> Result, + config: BackendConfig, +) -> Result<(), String> { + if let Some(v) = &mut *voice_state.inner.session.lock().unwrap() { + *v.backend_config.lock().unwrap() = config + } + + Ok(()) +} diff --git a/src-tauri/src/commands/server_list.rs b/src-tauri/src/commands/server_list.rs index b084088..176ae7f 100644 --- a/src-tauri/src/commands/server_list.rs +++ b/src-tauri/src/commands/server_list.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fs}; +use std::fs; use tauri::{AppHandle, Manager}; use crate::types::ServerMeta; @@ -7,12 +7,13 @@ use crate::types::ServerMeta; #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct KnownServer { - pub meta: ServerMeta, + pub hostname: String, pub public_key: String, pub is_secure: bool, + pub meta: ServerMeta, } -type ServerList = HashMap; +type ServerList = Vec; fn servers_file_path(app: &AppHandle) -> Result { let dir = app diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 73dbfc8..69fb5d8 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -24,6 +24,7 @@ pub fn run() { commands::config::update_config, commands::config::save_config, commands::config::get_config, + commands::config::update_backend_config, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/app/app.ts b/src/app/app.ts index a5abb25..c1f3a8b 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -10,7 +10,8 @@ import { Page } from "@/components/page/PageView"; import { Channel } from "@/lib/types"; import { invoke } from "@tauri-apps/api/core"; import { ClientMethod } from "./protocol"; -import { Config, getConfig } from "@/lib/config"; +import { BackendConfig, Config, getConfig } from "@/lib/config"; +import * as sfx from "@/lib/soundEffects"; ed.hashes.sha512 = sha512; @@ -29,17 +30,23 @@ ed.hashes.sha512 = sha512; */ export default class Enclave

{ public accounts?: AccountsFile; + public publicKey?: string; public server?: EnclaveServer; public serverList: ServerList; public isSettingsOpen: boolean; public forceRender: () => void; public page?: P; public config?: Config; + public backendConfig: BackendConfig; public constructor() { - this.serverList = {}; + this.serverList = []; this.forceRender = () => {}; this.isSettingsOpen = false; + this.backendConfig = { + isMuted: false, + isDeaf: false, + }; } public async init() { @@ -49,7 +56,15 @@ export default class Enclave

{ } public getAccount(): Account | null { - return this.accounts?.accounts[this.accounts.activeAccount] || null; + const account = this.accounts?.accounts[this.accounts.activeAccount]; + + if (account) { + this.publicKey = base58.encode( + ed.getPublicKey(base58.decode(account.privateKey)), + ); + } + + return account || null; } public getClientSecretKey() { @@ -113,13 +128,17 @@ export default class Enclave

{ return; } + const serveridx = this.serverList.findIndex( + (server) => server.hostname === hostname, + ); + if ( - this.serverList[hostname] && - this.serverList[hostname].publicKey !== + serveridx >= 0 && + this.serverList[serveridx].publicKey !== base58.encode(this.server.serverPublicKey) ) { console.error("Server's new public key doesn't match old one", { - old: this.serverList[hostname].publicKey, + old: this.serverList[serveridx].publicKey, new: base58.encode(this.server.serverPublicKey), }); return; @@ -156,12 +175,19 @@ export default class Enclave

{ this.server.meta = metaResponse.data as any; - this.serverList[hostname] = { - meta: metaResponse.data as any, - isSecure, + const serverItem = { + hostname, publicKey: base58.encode(this.server.serverPublicKey), + isSecure, + meta: metaResponse.data as any, }; + if (serveridx >= 0) { + this.serverList[serveridx] = serverItem; + } else { + this.serverList.push(serverItem); + } + this.forceRender(); } @@ -204,6 +230,8 @@ export default class Enclave

{ server.voiceChatSpeakers = {}; this.forceRender(); + + sfx.playLeave(); } public async onMessage(msg: ClientMethod) { @@ -224,13 +252,28 @@ export default class Enclave

{ return; case "Messages": + const currentTimestamp = Date.now(); + let shouldPlaySfx = false; + const authors = Object.entries(msg.messages).flatMap( ([channelId, messages]) => { if (!server.messages[channelId]) { server.messages[channelId] = {}; } + const page = this.page as Page | undefined; + + const isInvisibleChannel = page?.channel.id !== channelId; + return messages.map((message) => { + if ( + Math.abs(currentTimestamp - message.timestamp) < 1000 && + (isInvisibleChannel || document.visibilityState === "hidden") && + message.author !== this.publicKey + ) { + shouldPlaySfx = true; + } + server.messages[channelId][message.id] = message; return message.author; }); @@ -243,6 +286,8 @@ export default class Enclave

{ this.forceRender(); + if (shouldPlaySfx) sfx.playMessage(); + return; case "Users": @@ -274,6 +319,11 @@ export default class Enclave

{ server.voiceChatUsers[msg.channel_id].push(msg.pubkey); this.server?.getUsers([msg.pubkey]); + + if (msg.channel_id === this.server?.voiceChannelId) { + sfx.playJoin(); + } + return; case "UserLeftVoice": @@ -285,6 +335,10 @@ export default class Enclave

{ this.forceRender(); + if (msg.channel_id === this.server?.voiceChannelId) { + sfx.playLeave(); + } + return; case "Speaking": diff --git a/src/assets/join.wav b/src/assets/join.wav new file mode 100644 index 0000000..0e645ff Binary files /dev/null and b/src/assets/join.wav differ diff --git a/src/assets/leave.wav b/src/assets/leave.wav new file mode 100644 index 0000000..c6760be Binary files /dev/null and b/src/assets/leave.wav differ diff --git a/src/assets/message.mp3 b/src/assets/message.mp3 new file mode 100644 index 0000000..99094b2 Binary files /dev/null and b/src/assets/message.mp3 differ diff --git a/src/components/view/ServerList.tsx b/src/components/view/ServerList.tsx index 2e1da81..f23fd30 100644 --- a/src/components/view/ServerList.tsx +++ b/src/components/view/ServerList.tsx @@ -12,14 +12,14 @@ export default function ServerList({ return (