Compare commits
2 Commits
e2b957b6bd
...
fb66c0ed90
| Author | SHA1 | Date | |
|---|---|---|---|
| fb66c0ed90 | |||
| 9deeef6e8d |
@@ -8,6 +8,19 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
|
||||
## 🚩 Critical & UI Bugs
|
||||
|
||||
### 12. PiP Mute Icon Misidentifies Whose Mic Is Muted
|
||||
|
||||
- **File:** `cinny/src/app/components/CallEmbedProvider.tsx`
|
||||
- **Status:** **FIXED ⚠️ UNTESTED** — needs verification in a live call with at least one other participant who mutes/unmutes
|
||||
- **Issue:** The muted-mic badge in the Picture-in-Picture window used `useRemoteAllMuted` (fires when ANY remote participant is muted) and rendered in the bottom-left corner — the conventional position for "YOUR" mic status. Users read it as their own mic being muted.
|
||||
- **Root Cause:** `PipMuteOverlay` was triggering on remote-mute events while displaying in a position that implies local-user status.
|
||||
- **Fix Applied:**
|
||||
- **Bottom-left badge** now shows only when the LOCAL user's mic is muted (checked via `!controlState.microphone` from `useCallControlState`). Includes "You" label to make it unambiguous. Uses `color.Critical.Main`.
|
||||
- **Top-right badge** (new) shows "All muted" in `color.Warning.Main` when all remote participants are muted — positioned and labeled so it's clearly about other people, not the local user.
|
||||
- Both badges use `aria-label` / `title` for accessibility.
|
||||
|
||||
---
|
||||
|
||||
### 1. No Camera Focus During Screenshare
|
||||
|
||||
- **File:** `cinny/src/app/features/call/CallControls.tsx`
|
||||
|
||||
@@ -419,34 +419,61 @@ function CallUtils({ embed }: { embed: CallEmbed }) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Shown inside the PiP window when the local microphone is muted. */
|
||||
/**
|
||||
* PiP status indicators:
|
||||
* - Bottom-left badge: local mic muted (matches Discord/Slack convention — bottom-left = "your" mic)
|
||||
* - Top-right badge: all remote participants are muted (quiet room warning)
|
||||
*
|
||||
* Deliberately separated so users never mistake remote-mute state for their own.
|
||||
*/
|
||||
function PipMuteOverlay({ callEmbed }: { callEmbed: CallEmbed }) {
|
||||
const allMuted = useRemoteAllMuted(callEmbed);
|
||||
if (!allMuted) return null;
|
||||
const mx = useMatrixClient();
|
||||
const controlState = useCallControlState(callEmbed.control);
|
||||
const allRemoteMuted = useRemoteAllMuted(callEmbed);
|
||||
|
||||
const localMicMuted = !controlState.microphone;
|
||||
const localUserId = mx.getSafeUserId();
|
||||
const localDisplayName = getMxIdLocalPart(localUserId) ?? localUserId;
|
||||
|
||||
const badgeStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
zIndex: 3,
|
||||
background: 'rgba(0,0,0,0.65)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
borderRadius: '6px',
|
||||
padding: '3px 7px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
pointerEvents: 'none',
|
||||
fontSize: '12px',
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-label="Microphone muted"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: '8px',
|
||||
left: '8px',
|
||||
zIndex: 3,
|
||||
background: 'rgba(0,0,0,0.60)',
|
||||
backdropFilter: 'blur(4px)',
|
||||
borderRadius: '6px',
|
||||
padding: '3px 7px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px',
|
||||
pointerEvents: 'none',
|
||||
color: color.Critical.Main,
|
||||
fontSize: '13px',
|
||||
lineHeight: 1,
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Icon size="100" src={Icons.MicMute} filled />
|
||||
</div>
|
||||
<>
|
||||
{localMicMuted && (
|
||||
<div
|
||||
aria-label={`Your microphone is muted (${localDisplayName})`}
|
||||
title={`Your microphone is muted`}
|
||||
style={{ ...badgeStyle, bottom: '8px', left: '8px', color: color.Critical.Main }}
|
||||
>
|
||||
<Icon size="100" src={Icons.MicMute} filled />
|
||||
<span style={{ fontSize: '11px', fontWeight: 600 }}>You</span>
|
||||
</div>
|
||||
)}
|
||||
{allRemoteMuted && (
|
||||
<div
|
||||
aria-label="All other participants are muted"
|
||||
title="All other participants are muted"
|
||||
style={{ ...badgeStyle, top: '8px', right: '8px', color: color.Warning.Main }}
|
||||
>
|
||||
<Icon size="50" src={Icons.MicMute} />
|
||||
<span style={{ fontSize: '11px' }}>All muted</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -725,7 +725,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||
info: { mimetype: 'image/gif', w, h, size: blob.size },
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('GIF send failed', e);
|
||||
console.error('GIF send failed:', e instanceof Error ? e.message : 'unknown error');
|
||||
if (!alive()) return;
|
||||
setGifError('Failed to send GIF. Please try again.');
|
||||
setTimeout(() => setGifError(null), 4000);
|
||||
|
||||
@@ -50,7 +50,7 @@ export const getImageMsgContent = async (
|
||||
): Promise<IContent> => {
|
||||
const { file, originalFile, encInfo, metadata } = item;
|
||||
const [imgError, imgEl] = await to(loadImageElement(getImageFileUrl(originalFile)));
|
||||
if (imgError) console.warn(imgError);
|
||||
if (imgError) console.warn('Failed to load image element:', imgError.message);
|
||||
|
||||
const content: IContent = {
|
||||
msgtype: MsgType.Image,
|
||||
@@ -85,7 +85,7 @@ export const getVideoMsgContent = async (
|
||||
const { file, originalFile, encInfo, metadata } = item;
|
||||
|
||||
const [videoError, videoEl] = await to(loadVideoElement(getVideoFileUrl(originalFile)));
|
||||
if (videoError) console.warn(videoError);
|
||||
if (videoError) console.warn('Failed to load video element:', videoError.message);
|
||||
|
||||
const content: IContent = {
|
||||
msgtype: MsgType.Video,
|
||||
@@ -109,7 +109,7 @@ export const getVideoMsgContent = async (
|
||||
scaleYDimension(videoEl.videoWidth, 512, videoEl.videoHeight),
|
||||
);
|
||||
}
|
||||
if (thumbError) console.warn(thumbError);
|
||||
if (thumbError) console.warn('Failed to generate video thumbnail:', thumbError.message);
|
||||
content.info = {
|
||||
...getVideoInfo(videoEl, file),
|
||||
...thumbContent,
|
||||
|
||||
@@ -390,7 +390,7 @@ export class CallEmbed {
|
||||
if (this.call === null) return;
|
||||
const raw = ev.getEffectiveEvent();
|
||||
this.call.feedStateUpdate(raw as IRoomEvent).catch((e) => {
|
||||
console.error('Error sending state update to widget: ', e);
|
||||
console.error('Error sending state update to widget:', e instanceof Error ? e.message : 'unknown error');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -496,7 +496,7 @@ export class CallEmbed {
|
||||
} else {
|
||||
const raw = ev.getEffectiveEvent();
|
||||
this.call.feedEvent(raw as IRoomEvent).catch((e) => {
|
||||
console.error('Error sending event to widget: ', e);
|
||||
console.error('Error sending event to widget:', e instanceof Error ? e.message : 'unknown error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user