diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx index 49362b1de..0ba27e4da 100644 --- a/src/app/components/CallEmbedProvider.tsx +++ b/src/app/components/CallEmbedProvider.tsx @@ -48,6 +48,7 @@ import { useCallMembersChange, useCallSession } from '../hooks/useCall'; import { useCallJoinLeaveSounds } from '../hooks/useCallJoinLeaveSounds'; import { useCallPolicyRevokedToast } from '../hooks/useCallPolicyRevokedToast'; import { useCallAnnouncements } from '../hooks/useCallAnnouncements'; +import { useMutedTalkWarning } from '../hooks/useMutedTalkWarning'; import { callAnnouncementAtom } from '../state/callAnnouncement'; import { SrOnly } from '../features/call-status/styles.css'; import { useCallHotkeys } from '../hooks/useCallHotkeys'; @@ -744,6 +745,7 @@ function CallUtils({ embed, joined }: { embed: CallEmbed; joined: boolean }) { useCallJoinLeaveSounds(embed); useCallPolicyRevokedToast(embed, joined); useCallAnnouncements(embed, joined); + useMutedTalkWarning(embed, joined); useCallThemeSync(embed); useCallQuality(embed); useCallHangupEvent( diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index b26c73c1f..30208416f 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -1648,6 +1648,7 @@ function Calls() { const [deafenHotkey, setDeafenHotkey] = useSetting(settingsAtom, 'deafenHotkey'); const [globalCallHotkeys, setGlobalCallHotkeys] = useSetting(settingsAtom, 'globalCallHotkeys'); const [afkAutoMute, setAfkAutoMute] = useSetting(settingsAtom, 'afkAutoMute'); + const [mutedTalkWarning, setMutedTalkWarning] = useSetting(settingsAtom, 'mutedTalkWarning'); const [afkTimeoutMinutes, setAfkTimeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes'); const [callJoinLeaveSound, setCallJoinLeaveSound] = useSetting( settingsAtom, @@ -2012,6 +2013,13 @@ function Calls() { direction="Column" gap="400" > + + } + /> (null); + + // Unmuting (by any means) re-arms the warning and retires the toast. + useEffect(() => { + if (!microphone) return; + warnedThisMute.current = false; + if (toastIdRef.current) { + dismissToast(toastIdRef.current); + toastIdRef.current = null; + } + }, [microphone, dismissToast]); + + useEffect(() => { + if (!embed || !joined || !enabled || pttMode || microphone) return undefined; + const me = mx.getSafeUserId(); + let since: number | null = null; + const id = window.setInterval(() => { + if (warnedThisMute.current) return; + const local = embed.getLotusParticipants()?.find((p) => p.userId === me); + if (!local?.speakingWhileMuted) { + since = null; + return; + } + if (since === null) since = Date.now(); + if (Date.now() - since < HOLD_MS) return; + warnedThisMute.current = true; + const toastId = `muted-talk-${Date.now()}`; + toastIdRef.current = toastId; + setToast({ + id: toastId, + displayName: 'Lotus Chat', + body: "You're muted — click to unmute", + roomName: embed.room.name ?? 'Voice call', + roomId: embed.roomId, + sticky: true, + onClick: () => { + embed.control.setMicrophone(true); + }, + }); + }, CHECK_MS); + return () => window.clearInterval(id); + }, [embed, joined, enabled, pttMode, microphone, mx, setToast]); +} diff --git a/src/app/plugins/call/CallEmbed.ts b/src/app/plugins/call/CallEmbed.ts index f6dd11e9a..4d8b8b389 100644 --- a/src/app/plugins/call/CallEmbed.ts +++ b/src/app/plugins/call/CallEmbed.ts @@ -45,6 +45,8 @@ export interface LotusCallParticipant { speaking: boolean; audioEnabled: boolean; videoEnabled: boolean; + /** [lotus #37] Local participant only: voice on the mic while it is muted. */ + speakingWhileMuted?: boolean; } export class CallEmbed { diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index aefee0179..360712bc9 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -286,6 +286,8 @@ export interface Settings { fontFamily: 'system' | 'inter' | 'jetbrains-mono' | 'fira-code'; afkAutoMute: boolean; + /** [Gitea #117] 'You're muted' nudge when talking into a muted mic. */ + mutedTalkWarning: boolean; afkTimeoutMinutes: number; callJoinLeaveSound: 'off' | 'chime' | 'soft' | 'retro'; @@ -402,6 +404,7 @@ const defaultSettings: Settings = { fontFamily: 'inter', afkAutoMute: false, + mutedTalkWarning: true, afkTimeoutMinutes: 10, callJoinLeaveSound: 'chime',