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',