From c427231858b2384dee79d947abd430c432055fe9 Mon Sep 17 00:00:00 2001 From: Mollusk Date: Sun, 19 Jul 2026 02:02:03 -0400 Subject: [PATCH] feat(notifications): add chat and contact sounds --- CHANGELOG.md | 11 ++ assets/sounds/chat-received.wav | Bin 0 -> 17324 bytes assets/sounds/chat-sent.wav | Bin 0 -> 5804 bytes assets/sounds/contact-offline.wav | Bin 0 -> 25964 bytes assets/sounds/contact-online.wav | Bin 0 -> 25964 bytes assets/sounds/generate_chimes.py | 8 + src/app/mod.rs | 262 +++++++++++++++++++++++++++--- src/config.rs | 36 ++++ src/notify.rs | 26 ++- 9 files changed, 316 insertions(+), 27 deletions(-) create mode 100644 assets/sounds/chat-received.wav create mode 100644 assets/sounds/chat-sent.wav create mode 100644 assets/sounds/contact-offline.wav create mode 100644 assets/sounds/contact-online.wav diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2ad40..33e056f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to PeerSpeak are documented here. ## [Unreleased] +### Added +- **Chat message sounds.** Successful outgoing messages and admitted incoming + messages now have distinct notification chimes, each with its own enable + toggle and optional custom WAV path in Notifications settings. +- **Contact presence sounds.** The home-screen contacts list now announces a + contact becoming online or offline. Initial online contacts are announced; + initial offline results stay silent. Both events have independent toggles and + optional custom WAV paths. +- **Notification sound browser.** Every notification event now has a native + Browse button for choosing a custom WAV instead of typing its path manually. + ### Fixed - **Low-latency screen sharing stays near the live edge again.** mpv's timestamp pacing could let stale frames accumulate across the reliable diff --git a/assets/sounds/chat-received.wav b/assets/sounds/chat-received.wav new file mode 100644 index 0000000000000000000000000000000000000000..5efc9695c421b491340fc3925ae1047644bce103 GIT binary patch literal 17324 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 UrO#$=WVK_TWnX6(vwyMv53qU8+W-In literal 0 HcmV?d00001 diff --git a/assets/sounds/chat-sent.wav b/assets/sounds/chat-sent.wav new file mode 100644 index 0000000000000000000000000000000000000000..83dd150c46eb140980d36a97cb7ba60e91a906b8 GIT binary patch literal 5804 zcmWNVRd`fa7lzLn_nC~(oHL$DLJKVxv>{NWK!8ASr??jhQmhG93Y1_WcyTBcrvxWZ z{@|2QB$1$P$Ew^O0Kfw3lXNm{y9q4Nt zI2$?{#U=mFYy&4?(Zs2gq4XPhYgxzH0#2CSi%n*&$~#6sLj9fm8}T&Wmg}BCJp>N| zYmwnuLS}EuoJhnzMK*@mfm7b0?yky;%s5{g~xSl_rYhab+)zBj3EyQyC7;GIHkJ<|;k+YdhN|^M=)F8w^cN?&e#YaVL9<8}pZ$4r?Hm>HxVjJ2GIV55jAt&%xpV`LV|T@eVm zdFR=a^E%K<$a@K$a1f?DY87xVYfh_@+v56&F1XW|at*a#H1{)X|yA z8)km!VEUP{HQ5Zdg4&KfM7UB?r6@0GDJ&=&Qn*CfAYUdq3tiNZJOZVqX3#S!LEBdZbtn4ap5C?=HPr_PCXHw|Ixi}Uk34#DA8%!NfB%_JYF@Mm_ zcC@ifF}O6uR!{SrFK@n(TlTlk)Xg$4ah3<3#@7KI3H#|2xZlH&>`}h3uw4w^uloAnZjNLUWHXUMYKzBiDSsCpmE8? zgsa#}^dj&VWLsu_ikui19TYP9I=Ls>tIQwuyEJAMwWVwGZ_PqgK#e!_u~xe0hcwAe zs60~Fyc@i}qK7g-nJK6*>{EDOIa2YR^a`xwV>koyo>PyK-sU(dM7IG;k%5_l)RQJM)5eR2WzZ_WGlGd0aBMN4I~ula)NtVV6(u*kQ{b>3z$jn$WG-m6Mm zZa0r_>Crk_d&?-dukrp8@uk;dTFL(~ef&1!e)2-)^MZDTiwZ913*`>+2pgdDxC_t2o{B^S4;xj@mSH#*z??L&VcosJvQ-+!WY|Q?e z9+%X`=n-pRvG=hvX)QBZbcp($YIDn=mY%8w>d(3bW}=&LJqvhByrM+vg@+0D za;$laXuZh`2p;Tx^b^p6;IjDiyTlK%dEw51dJow-%GzMusC%#0s?;ssmiDceG!qS9 zSwpVsU|qrubR)E<@8&?zNeQUycVjfDG|(0qn(3cP#ivFWg}(Ct>Bc$cThKSjDIDhPe>4R_zLOD(SqX6-3;=hkPcC9P$evHGp%`;J$>HPP*v8<>maw#)=i z17DMlS2X2Il;iVH$qS_d(O&`T7nXqcP%B_fk_n&NW)$9%6sEZ;3z z4HE>FIZ8Iu=8%sO3UOI99n}Z;IeQ>ooy?C{MwSHSzLPGxeZP6S;ga@@dQI!d)*kil7fyMFV$a-9V>aQ$jlXXsFd*QlD-;)!LvY>V7t6t;^jPgR>ITfk}8W ztu1@2z$xk~o1_?%-zlH2pvk6*yFkx)J=jkeYpLr=AMk6j^U=S9uMl(=PNx%lW1qs$ z1C`#GbD524#u+wh>(x!Ik=AaSv$|r_N84o2@z8{1IoOr(n)aHV6?7GklZ{X)@->Ph z^3BrcqB^0HcY@WCk)%*aOYucm1$r1*lcQUkx|bLf+Y(+D5PQ!!3vC}w8vSzZb2Uai zM14!sPaih@Wgp<(6CRrChN2R;(r0ph5^fMLm-SJ&6`K@Fc|!7yC@$E_C9=-Z*HMlW zS@Xi69 zY6-paS9(BaoprjYR{x_mtX`$YYwl=w8dh7TI)(n_(Y6@?^B2j@*vu`3;^I0PQ?W$x zKY0h4R-A?&@XK?KbAYy;{F*QYSArRfssoH!VMdwq#TP_tLo5B#oZ>62--RL5^GNvCNLw$&+0HM$aQ&S-f^g{$YIEq586L?9)}8&FM#Rz>(rI3b^KcRtmKGnm3)9)F6$sU4j&R!aHGsC z^g2o-u^*m+m7phsHxN{|L;9VMW=)#x?(x{Zdgd8Xr< z_gt8sx&XE(%%+jqPJS!=L{cN`FAvIYN-v8kFetdsnZ+!kiz$%(}>mI)sE1*wRQS2CXubl zRTdZ0oa3;CJ46vlnQXtzBpoLaipB~{d0EzD#%by^(l-Pf)`}LPRsdhJ z`I!N!Hi@TEYM33cd(Jyo+N#Y_14jQ@yIVU{Cp5%OD%&Y{n_!Rl!7K(_LdNDPxC?~$ zL}|&_vJtY4QoVRD+$gxjU7Vw&kUEC+FTNIg0<8yo0VlJbG&yCDug&?{Gyh@F6lXWv zNVC>}*T=R0X|L-}7>?yAsr2AOP@)p~fQ^v<$UDM)AT)_G5}b@AlSpTaiEw8DkE>%| zr(dBsiOcWN8dprGa@_f8gv@)m4As((XZ7njtG*I;-olqy#w=wp!jC1_p zHHT7(p}VDPVHhL{Wr_lE#+?-T^)A3`d|6{J<4TJK< ze#tfIGU;&1Vo^lM;z!ue^ZueeB}2pp+!c%p)d4(~qx*;S*rXzUI?@_!@!fLobnLLU znl2g&^p(1sx|n`|>9AGf^!ScOZl$7NJ^`SWvkbglP>I+m*&vlkEn+{sK)8uFo82q# z8`^O49m0IvtQ>&}U@r1DLr)8n`q;!sO>P#M?;h+JW4&WKVd$nmr^D&z8(K^QZ6{p~ z{#DUk=|+^1aF+Iw^*#S3G+g|zWT5noWVLuHtQUm2&8&LHb*h0hmcYY_F_TeGfzHUG z%$JlVaV*A+^bMB!WNwl}VLfCzY#63*)lJeH4U5fj+ids#z=+ta%s%u{Vn6yQ_Hlj| znj)^3(4;FQM6n+lEttg}l5?D{)D@&Sz7gAkmZ5e6ATl%aN9sgkLhMyI76^G;T(9jR z%LY@OVXmH|zpbBS#9J=f+j-^$+r)chCt)f`I{LrtFuxrFhND+STuPBqc5kQU@W{qyhc(Zc_>zjJi^}mA)M~aZ|P-})5N~`ud$=hS3o&% zIGaj;o61joh;|SE7?|Q6<{D)`YAH7zF`U=$)2}p4H1)Q!oe#X!@aKdEF=Mahlyj3) zBUlH|6OWb*mP`>}&)wv0UIY6^-ZfeaxfjumGho>0m7pJ)n{7--lh*jIs6WIFaJ*y} zY_GJGm`)qs>YMeg2AxT7edFBh(}gc4{{&9q#!>d)YggV?o%x6?7_z)@1?oB_;DQ9r>eCW0RjpwD)YAd&VW4daH>Scz0#@XhL zw#}}g{_Bx-spa5ge2T(jZsf9sf5YR%i1@a+PV@|#DLBo&%(}?9Pfd~55&GdOFo#e? zaC7FPy;V|_O z)4(kkzJdpd--_pm+l$H|Js;v0TZ&l5XMq5Q&1m)aY$39C_N}y6mN)dL*4wP zo-${(jb?#NO@>j1M+T{Bz2&Vv;nw8{6lM@qBcYhaVfEx~7OLQ4@j)?H`~va`Uigw`x>o8od61vKQf^dJ!#MV54;QdeMvXXInow1b4_-`Zw97upNVQ+ z>A3B=5R}BNnFjO<;w#!tRt@i#&;rBaY2qiM$?#ZV3y;Eqm~H73DNl$C@Ds4x(Qa@N z5X~;iJWPE`T!?jvYz;{aKG?AWV z>3CY93r0i=@qAGLQVT}&wz3cAC~=Xe5ZO7WEJObd_5>QUJu}-ww|0OG~ixfsOVonB2b$pW-3xs5{0q9a;k)Uo!zq>pRCu+BGdQA5~ISDHNUp4 zaru0x$fjfuz=xen{)zE{y@;O=IpDjZ6QWx%3o`SEao4jBG8(9PAB z%*uXA!zo3=8l4lq7WnK9yV^UhTThq^OmmD?#x1#NR@z$oTi z=J$pS@NUrr(HQu&a1Q?sC&d&p%BUAeeFzdJnXfq&-cKAW~AZSk{Fd3au6kGI~H zwbxkpnLC^dZ^sP1p>23d#(u$xW5Z(^qn7ZDv#x$`1_lu64b$FR^Yn_cc8xd5pkBJ|K0{|--+bBH}Z*L$03oC#+>Zjsrm zTxWdq!Y2~~hkA}Wm3D@8i}{>spJ|GjWxZ-wy2twqBh!4Y7`z6G2^_rDoL5Xdqn!GH^dn&aZV~1UYBU%|_GbB+%2ZwA=NKH>7JTnx zc;-1lJ7C>ueqm}d-8FBw_IA8-(*l-=JhcmmVx{C(dV)2UrxvV%I>J;~2K_Er&P%a} zGI!Cnlrba%A&LDOvmd1Z8UkD89o47$gMZ*|kw-H|vG?%8f~61n15@~ literal 0 HcmV?d00001 diff --git a/assets/sounds/contact-offline.wav b/assets/sounds/contact-offline.wav new file mode 100644 index 0000000000000000000000000000000000000000..bc67ce01c3041d058828d3935d74d50e9c646ccf GIT binary patch literal 25964 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 A-~a#s literal 0 HcmV?d00001 diff --git a/assets/sounds/contact-online.wav b/assets/sounds/contact-online.wav new file mode 100644 index 0000000000000000000000000000000000000000..ea8b4fa7430208a08f5ab73504fb420f45429cc0 GIT binary patch literal 25964 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 yCsX;xVQagy7M947xLC;m literal 0 HcmV?d00001 diff --git a/assets/sounds/generate_chimes.py b/assets/sounds/generate_chimes.py index e46e500..8cecbe3 100644 --- a/assets/sounds/generate_chimes.py +++ b/assets/sounds/generate_chimes.py @@ -76,6 +76,14 @@ CHIMES = { "mic-toggle.wav": [(E5, 0.08)], # Reconnect gave up: disappointing low two-note fall. "reconnect-failed.wav": [(C5, 0.15), (349.23, 0.30)], + # Our chat message entered the room: a tiny bright acknowledgement. + "chat-sent.wav": [(1046.50, 0.06)], + # A peer message arrived: a soft two-note lift, distinct but unobtrusive. + "chat-received.wav": [(E5, 0.07), (G5, 0.11)], + # A saved contact came online: a light, higher two-note arrival. + "contact-online.wav": [(E5, 0.09), (880.00, 0.18)], + # A saved contact went offline: the same tonal family falling away. + "contact-offline.wav": [(E5, 0.09), (440.00, 0.18)], } diff --git a/src/app/mod.rs b/src/app/mod.rs index 5236b59..1375437 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -760,6 +760,10 @@ pub enum AppMessage { ToggleNotifications(bool), ToggleEchoCancellation(bool), CustomSoundPathChanged(Sound, String), + /// Open a native WAV picker for one notification event. + BrowseCustomSound(Sound), + /// Result of the notification WAV picker (`None` = cancelled). + CustomSoundFilePicked(Sound, Option), /// Toggle the per-sound enable flag for a single chime (W6). ToggleSoundEnabled(Sound, bool), /// Open / cancel the "Regenerate identity?" confirm modal (W7). @@ -1339,16 +1343,20 @@ impl AppState { /// so Retry can re-dispatch, unless the entry is already gone (history /// eviction / room reset), in which case the payload is dropped so its map /// can't leak. Either way an id with no matching entry is a harmless no-op. - fn apply_send_result(&mut self, local_id: u64, error: Option) { + /// Returns `true` only when a successful result matched a live local echo, + /// which is the boundary used for the outgoing-message notification. + fn apply_send_result(&mut self, local_id: u64, error: Option) -> bool { match error { None => { - self.set_send_status(local_id, SendStatus::Broadcast); + let matched = self.set_send_status(local_id, SendStatus::Broadcast); self.send_payloads.remove(&local_id); + matched } Some(e) => { if !self.set_send_status(local_id, SendStatus::Failed(e)) { self.send_payloads.remove(&local_id); } + false } } } @@ -1379,9 +1387,30 @@ impl AppState { Sound::SelfLeave => &self.config.custom_sound_self_leave, Sound::MicToggle => &self.config.custom_sound_mic_toggle, Sound::ReconnectFailed => &self.config.custom_sound_reconnect_failed, + Sound::ChatSent => &self.config.custom_sound_chat_sent, + Sound::ChatReceived => &self.config.custom_sound_chat_received, + Sound::ContactOnline => &self.config.custom_sound_contact_online, + Sound::ContactOffline => &self.config.custom_sound_contact_offline, }; opt.as_deref().unwrap_or("") } + + fn set_custom_sound_path(&mut self, sound: Sound, path: Option) { + match sound { + Sound::SelfJoin => self.config.custom_sound_self_join = path, + Sound::PeerJoin => self.config.custom_sound_peer_join = path, + Sound::PeerLeave => self.config.custom_sound_peer_leave = path, + Sound::ReconnectAttempt => self.config.custom_sound_reconnect_attempt = path, + Sound::Reconnected => self.config.custom_sound_reconnected = path, + Sound::SelfLeave => self.config.custom_sound_self_leave = path, + Sound::MicToggle => self.config.custom_sound_mic_toggle = path, + Sound::ReconnectFailed => self.config.custom_sound_reconnect_failed = path, + Sound::ChatSent => self.config.custom_sound_chat_sent = path, + Sound::ChatReceived => self.config.custom_sound_chat_received = path, + Sound::ContactOnline => self.config.custom_sound_contact_online = path, + Sound::ContactOffline => self.config.custom_sound_contact_offline = path, + } + } } impl Default for AppState { @@ -1871,6 +1900,47 @@ fn reconnected_chime( was_reconnect.then_some(Sound::Reconnected) } +/// Return the landing-page contact chime for one definitive presence update. +/// An initial online result is an arrival (so contacts already online at app +/// startup are announced), while an initial offline result is silent. Online +/// includes both plain `Online` and `InRoom`; moving between those two states is +/// not a connection transition. Updates continue to populate the presence map +/// off-home, but notification sounds are intentionally limited to the home page. +fn friend_presence_notification( + screen: Screen, + previous: Option<&crate::presence::FriendPresence>, + next: &crate::presence::FriendPresence, +) -> Option { + if screen != Screen::Home { + return None; + } + + let online = |presence: &crate::presence::FriendPresence| { + matches!( + presence, + crate::presence::FriendPresence::Online + | crate::presence::FriendPresence::InRoom { .. } + ) + }; + match (previous.map(online), online(next)) { + (None | Some(false), true) => Some(Sound::ContactOnline), + (Some(true), false) => Some(Sound::ContactOffline), + _ => None, + } +} + +/// Convert a native picker result into the persisted notification path. The +/// dialog filter is advisory on some desktops, so enforce WAV here as well. +/// `None` (cancel) and a non-WAV selection leave the existing setting untouched. +fn selected_wav_path(picked: Option) -> Option { + let path = picked?; + let is_wav = path + .extension() + .and_then(|ext| ext.to_str()) + .is_some_and(|ext| ext.eq_ignore_ascii_case("wav")); + is_wav.then(|| path.to_string_lossy().into_owned()) +} + fn in_call(state: &AppState) -> bool { !state.ticket.is_empty() } @@ -2310,7 +2380,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { // failure it's retained for Retry — unless the entry is gone // (history eviction / room reset), in which case drop it so // the payload map can't leak. - state.apply_send_result(local_id, error); + if state.apply_send_result(local_id, error) { + notify::play( + Sound::ChatSent, + state.config.custom_sound_chat_sent.as_deref(), + ); + } } UiEvent::ChatMessage { from, @@ -2343,6 +2418,10 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { local_send: None, }, ); + notify::play( + Sound::ChatReceived, + state.config.custom_sound_chat_received.as_deref(), + ); } } UiEvent::AttachmentReady { from, id, data } => { @@ -2512,7 +2591,15 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { state.friends_read_only = read_only; } UiEvent::FriendPresence { id, presence } => { + let sound = friend_presence_notification( + state.current_screen, + state.friend_presence.get(&id), + &presence, + ); state.friend_presence.insert(id, presence); + if let Some(sound) = sound { + notify::play(sound, Some(state.custom_sound_path(sound))); + } } UiEvent::FriendsRescanned => { // The manual pass finished. Stamp the time for the live "scanned @@ -2887,15 +2974,36 @@ fn update(state: &mut AppState, message: AppMessage) -> Task { } else { Some(path) }; - match sound { - Sound::SelfJoin => state.config.custom_sound_self_join = path_opt, - Sound::PeerJoin => state.config.custom_sound_peer_join = path_opt, - Sound::PeerLeave => state.config.custom_sound_peer_leave = path_opt, - Sound::ReconnectAttempt => state.config.custom_sound_reconnect_attempt = path_opt, - Sound::Reconnected => state.config.custom_sound_reconnected = path_opt, - Sound::SelfLeave => state.config.custom_sound_self_leave = path_opt, - Sound::MicToggle => state.config.custom_sound_mic_toggle = path_opt, - Sound::ReconnectFailed => state.config.custom_sound_reconnect_failed = path_opt, + state.set_custom_sound_path(sound, path_opt); + } + AppMessage::BrowseCustomSound(sound) => { + let initial_dir = { + let current = state.custom_sound_path(sound); + (!current.trim().is_empty()) + .then(|| notify::expand_tilde(current)) + .and_then(|path| path.parent().map(std::path::Path::to_path_buf)) + .filter(|path| path.is_dir()) + }; + return Task::perform( + async move { + let mut dialog = rfd::AsyncFileDialog::new() + .add_filter("WAV audio", &["wav"]) + .set_title("Choose a notification sound"); + if let Some(dir) = initial_dir { + dialog = dialog.set_directory(dir); + } + dialog + .pick_file() + .await + .map(|handle| handle.path().to_path_buf()) + }, + move |picked| AppMessage::CustomSoundFilePicked(sound, picked), + ); + } + AppMessage::CustomSoundFilePicked(sound, picked) => { + if let Some(path) = selected_wav_path(picked) { + state.set_custom_sound_path(sound, Some(path)); + state.config.save(); } } AppMessage::ToggleSoundEnabled(sound, enabled) => { @@ -5282,10 +5390,19 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { ] .spacing(6) .align_y(iced::alignment::Vertical::Center), - context_input("Default (embedded)...", path) - .on_input(move |val| AppMessage::CustomSoundPathChanged(sound, val)) - .style(t_style) - .padding(8) + row![ + context_input("Default (embedded)...", path) + .on_input(move |val| AppMessage::CustomSoundPathChanged(sound, val)) + .style(t_style) + .padding(8) + .width(iced::Length::Fill), + button(text("Browse…").size(11)) + .on_press(AppMessage::BrowseCustomSound(sound)) + .style(b_style(color_surface, color_blue, color_text, 5.0)) + .padding([8, 10]), + ] + .spacing(6) + .width(iced::Length::Fill) ] .spacing(4) .width(iced::Length::Fill) @@ -6035,6 +6152,14 @@ fn view(state: &AppState) -> Element<'_, AppMessage> { path_field("Mic Toggle", Sound::MicToggle), path_field("Reconnect Failed", Sound::ReconnectFailed), ].spacing(20).width(iced::Length::Fill), + row![ + path_field("Chat Sent", Sound::ChatSent), + path_field("Chat Received", Sound::ChatReceived), + ].spacing(20).width(iced::Length::Fill), + row![ + path_field("Contact Online", Sound::ContactOnline), + path_field("Contact Offline", Sound::ContactOffline), + ].spacing(20).width(iced::Length::Fill), ].spacing(8).width(iced::Length::Fill), ] .spacing(10) @@ -9544,12 +9669,12 @@ mod tests { use super::sendqueue::{self, LocalSend, SendStatus}; use super::{ AppConfig, AppMessage, AppState, AttachmentCache, AttachmentState, - CLOCK_SKEW_WARNING_VISIBLE_SECS, ChatEntry, ClockSkewBanner, GateMeter, METER_MAX, + CLOCK_SKEW_WARNING_VISIBLE_SECS, ChatEntry, ClockSkewBanner, GateMeter, METER_MAX, Screen, ScreenBounds, UiEvent, attachment_default_name, clamp_window_position, clear_expired_clock_skew_warning, format_clock_skew_duration, format_duration, - format_relative_ago, initial_window_position, now_playing_label, reconnect_attempt_chime, - reconnected_chime, set_peer_gate_config, set_peer_volume_config, show_clock_skew_warning, - update, + format_relative_ago, friend_presence_notification, initial_window_position, + now_playing_label, reconnect_attempt_chime, reconnected_chime, selected_wav_path, + set_peer_gate_config, set_peer_volume_config, show_clock_skew_warning, update, }; use iroh::SecretKey; use std::collections::VecDeque; @@ -10640,6 +10765,91 @@ mod tests { const W: f32 = 200.0; + #[test] + fn initial_contact_presence_announces_only_online() { + use crate::presence::FriendPresence; + + assert_eq!( + friend_presence_notification(Screen::Home, None, &FriendPresence::Online), + Some(Sound::ContactOnline) + ); + assert_eq!( + friend_presence_notification(Screen::Home, None, &FriendPresence::Offline), + None + ); + } + + #[test] + fn contact_presence_chimes_only_on_online_boundary() { + use crate::presence::FriendPresence; + + let in_room = FriendPresence::InRoom { + name: "Game night".to_string(), + ticket: "ticket".to_string(), + }; + assert_eq!( + friend_presence_notification(Screen::Home, Some(&FriendPresence::Offline), &in_room,), + Some(Sound::ContactOnline) + ); + assert_eq!( + friend_presence_notification( + Screen::Home, + Some(&FriendPresence::Online), + &FriendPresence::Offline, + ), + Some(Sound::ContactOffline) + ); + assert_eq!( + friend_presence_notification(Screen::Home, Some(&FriendPresence::Online), &in_room,), + None + ); + assert_eq!( + friend_presence_notification( + Screen::Home, + Some(&FriendPresence::Offline), + &FriendPresence::Offline, + ), + None + ); + } + + #[test] + fn contact_presence_is_silent_away_from_landing_page() { + use crate::presence::FriendPresence; + + assert_eq!( + friend_presence_notification(Screen::Room, None, &FriendPresence::Online), + None + ); + assert_eq!( + friend_presence_notification( + Screen::Settings, + Some(&FriendPresence::Online), + &FriendPresence::Offline, + ), + None + ); + } + + #[test] + fn selected_notification_sound_accepts_wav_and_preserves_cancel() { + use std::path::PathBuf; + + assert_eq!(selected_wav_path(None), None); + assert_eq!( + selected_wav_path(Some(PathBuf::from("/tmp/notify.mp3"))), + None + ); + assert_eq!( + selected_wav_path(Some(PathBuf::from("/tmp/notify.wav"))), + Some("/tmp/notify.wav".to_string()) + ); + assert_eq!( + selected_wav_path(Some(PathBuf::from("/tmp/notify.WAV"))), + Some("/tmp/notify.WAV".to_string()) + ); + } + #[test] fn gate_drag_maps_left_edge_to_zero() { assert_eq!(GateMeter::x_to_threshold(0.0, W), 0.0); @@ -10923,7 +11133,7 @@ mod tests { // Empty queue + a fresh full pacer → dispatched immediately. assert_eq!(status_of(&state, id), Some(SendStatus::Pending)); assert!(state.send_payloads.contains_key(&id)); - state.apply_send_result(id, None); + assert!(state.apply_send_result(id, None)); assert_eq!(status_of(&state, id), Some(SendStatus::Broadcast)); // A completed send releases its retry payload. assert!(!state.send_payloads.contains_key(&id)); @@ -10934,7 +11144,7 @@ mod tests { let mut state = AppState::default(); let id = push_own(&mut state, "yo"); state.submit_send(id, PendingSend::Text("yo".to_string())); - state.apply_send_result(id, Some("not in a room".to_string())); + assert!(!state.apply_send_result(id, Some("not in a room".to_string()))); assert_eq!( status_of(&state, id), Some(SendStatus::Failed("not in a room".to_string())) @@ -10950,11 +11160,11 @@ mod tests { state.submit_send(a, PendingSend::Text("a".to_string())); let b = push_own(&mut state, "b"); state.submit_send(b, PendingSend::Text("b".to_string())); - state.apply_send_result(a, None); + assert!(state.apply_send_result(a, None)); assert_eq!(status_of(&state, a), Some(SendStatus::Broadcast)); assert_eq!(status_of(&state, b), Some(SendStatus::Pending)); // A result for an id with no matching entry is a harmless no-op. - state.apply_send_result(9999, None); + assert!(!state.apply_send_result(9999, None)); assert_eq!(status_of(&state, b), Some(SendStatus::Pending)); } @@ -10968,7 +11178,7 @@ mod tests { state .chat_messages .retain(|m| m.local_send.as_ref().map(|s| s.id) != Some(id)); - state.apply_send_result(id, Some("dead".to_string())); + assert!(!state.apply_send_result(id, Some("dead".to_string()))); // No entry to mark → the payload must not leak. assert!(!state.send_payloads.contains_key(&id)); } @@ -10983,7 +11193,7 @@ mod tests { assert!(state.send_queue.is_empty()); assert!(state.send_payloads.is_empty()); // A late result for the pre-reset send touches nothing and adds no entry. - state.apply_send_result(id, None); + assert!(!state.apply_send_result(id, None)); assert!(state.chat_messages.is_empty()); assert!(state.send_payloads.is_empty()); } diff --git a/src/config.rs b/src/config.rs index ce03759..8ca4a9e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -478,6 +478,14 @@ pub struct AppConfig { pub custom_sound_mic_toggle: Option, #[serde(default)] pub custom_sound_reconnect_failed: Option, + #[serde(default)] + pub custom_sound_chat_sent: Option, + #[serde(default)] + pub custom_sound_chat_received: Option, + #[serde(default)] + pub custom_sound_contact_online: Option, + #[serde(default)] + pub custom_sound_contact_offline: Option, /// Per-sound enable flags (W6). The master `notifications_enabled` toggle /// gates ALL chimes; these let the user silence individual events while the /// master stays on. A chime plays only if the master AND its flag are true. @@ -498,6 +506,14 @@ pub struct AppConfig { pub sound_mic_toggle_enabled: bool, #[serde(default = "default_true")] pub sound_reconnect_failed_enabled: bool, + #[serde(default = "default_true")] + pub sound_chat_sent_enabled: bool, + #[serde(default = "default_true")] + pub sound_chat_received_enabled: bool, + #[serde(default = "default_true")] + pub sound_contact_online_enabled: bool, + #[serde(default = "default_true")] + pub sound_contact_offline_enabled: bool, /// Optional override for the `pixelpass` binary location (screen share). /// Empty / unset = look it up on `$PATH`. Hand-editable; no Settings UI yet. #[serde(default)] @@ -601,6 +617,10 @@ impl Default for AppConfig { custom_sound_self_leave: None, custom_sound_mic_toggle: None, custom_sound_reconnect_failed: None, + custom_sound_chat_sent: None, + custom_sound_chat_received: None, + custom_sound_contact_online: None, + custom_sound_contact_offline: None, sound_self_join_enabled: true, sound_peer_join_enabled: true, sound_peer_leave_enabled: true, @@ -609,6 +629,10 @@ impl Default for AppConfig { sound_self_leave_enabled: true, sound_mic_toggle_enabled: true, sound_reconnect_failed_enabled: true, + sound_chat_sent_enabled: true, + sound_chat_received_enabled: true, + sound_contact_online_enabled: true, + sound_contact_offline_enabled: true, pixelpass_path: None, screen_share: ScreenShareSettings::default(), recents: Vec::new(), @@ -639,6 +663,10 @@ impl AppConfig { Sound::SelfLeave => self.sound_self_leave_enabled, Sound::MicToggle => self.sound_mic_toggle_enabled, Sound::ReconnectFailed => self.sound_reconnect_failed_enabled, + Sound::ChatSent => self.sound_chat_sent_enabled, + Sound::ChatReceived => self.sound_chat_received_enabled, + Sound::ContactOnline => self.sound_contact_online_enabled, + Sound::ContactOffline => self.sound_contact_offline_enabled, } } @@ -653,6 +681,10 @@ impl AppConfig { Sound::SelfLeave => self.sound_self_leave_enabled = enabled, Sound::MicToggle => self.sound_mic_toggle_enabled = enabled, Sound::ReconnectFailed => self.sound_reconnect_failed_enabled = enabled, + Sound::ChatSent => self.sound_chat_sent_enabled = enabled, + Sound::ChatReceived => self.sound_chat_received_enabled = enabled, + Sound::ContactOnline => self.sound_contact_online_enabled = enabled, + Sound::ContactOffline => self.sound_contact_offline_enabled = enabled, } } @@ -940,6 +972,10 @@ mod tests { assert!(deserialized.custom_sound_self_leave.is_none()); assert!(deserialized.custom_sound_mic_toggle.is_none()); assert!(deserialized.custom_sound_reconnect_failed.is_none()); + assert!(deserialized.custom_sound_chat_sent.is_none()); + assert!(deserialized.custom_sound_chat_received.is_none()); + assert!(deserialized.custom_sound_contact_online.is_none()); + assert!(deserialized.custom_sound_contact_offline.is_none()); assert_eq!(deserialized.screen_share, ScreenShareSettings::default()); assert_eq!(deserialized.screen_share.quality, ShareQuality::Auto); assert_eq!(deserialized.screen_share.player, SharePlayer::Mpv); diff --git a/src/notify.rs b/src/notify.rs index ad97253..1d7adcd 100644 --- a/src/notify.rs +++ b/src/notify.rs @@ -80,6 +80,14 @@ pub enum Sound { MicToggle, /// Reconnect failed / peer evicted. ReconnectFailed, + /// One of our chat messages was broadcast to the room. + ChatSent, + /// A chat message from another participant was admitted. + ChatReceived, + /// A saved contact was detected online on the home screen. + ContactOnline, + /// A saved contact previously seen online went offline on the home screen. + ContactOffline, } impl Sound { @@ -93,10 +101,14 @@ impl Sound { Sound::SelfLeave, Sound::MicToggle, Sound::ReconnectFailed, + Sound::ChatSent, + Sound::ChatReceived, + Sound::ContactOnline, + Sound::ContactOffline, ]; /// Number of distinct notification events. - pub const COUNT: usize = 8; + pub const COUNT: usize = 12; /// Stable 0-based index into the per-sound flag array. Must match `ALL`. fn index(self) -> usize { @@ -109,6 +121,10 @@ impl Sound { Sound::SelfLeave => 5, Sound::MicToggle => 6, Sound::ReconnectFailed => 7, + Sound::ChatSent => 8, + Sound::ChatReceived => 9, + Sound::ContactOnline => 10, + Sound::ContactOffline => 11, } } @@ -123,6 +139,10 @@ impl Sound { Sound::SelfLeave => include_bytes!("../assets/sounds/self-leave.wav"), Sound::MicToggle => include_bytes!("../assets/sounds/mic-toggle.wav"), Sound::ReconnectFailed => include_bytes!("../assets/sounds/reconnect-failed.wav"), + Sound::ChatSent => include_bytes!("../assets/sounds/chat-sent.wav"), + Sound::ChatReceived => include_bytes!("../assets/sounds/chat-received.wav"), + Sound::ContactOnline => include_bytes!("../assets/sounds/contact-online.wav"), + Sound::ContactOffline => include_bytes!("../assets/sounds/contact-offline.wav"), } } @@ -137,6 +157,10 @@ impl Sound { Sound::SelfLeave => "self-leave", Sound::MicToggle => "mic-toggle", Sound::ReconnectFailed => "reconnect-failed", + Sound::ChatSent => "chat-sent", + Sound::ChatReceived => "chat-received", + Sound::ContactOnline => "contact-online", + Sound::ContactOffline => "contact-offline", } } }