Compare commits

...

5 Commits

Author SHA1 Message Date
jared a77c4b6db5 feat(calls): configurable ringtone volume in Settings (Bug #4 partial)
CI / Build & Quality Checks (push) Successful in 10m29s
CI / Trigger Desktop Build (push) Successful in 15s
Adds 'Ringtone Volume' slider (0–100, default 70%) to Settings → Calls.
The IncomingCall audio element reads the setting and applies it as
audioElement.volume before playing, replacing the implicit browser
default of 1.0.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:56:04 -04:00
jared cb3d2c40e5 fix(a11y): descriptive aria-label on reaction buttons (P3-4)
Reaction.tsx now computes aria-label='{shortcode} reaction, N people'
using getShortcodeFor so screen readers announce emoji name and count
instead of an ambiguous button. Custom (mxc://) emoji falls back to
'custom emoji reaction'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:50:19 -04:00
jared f50e14d7a5 fix(a11y): label room page header with room name for screen readers (P3-4)
PageHeader now exposes aria-label="{room name} room header" so screen
reader users know which room's header they are navigating.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:46:30 -04:00
jared 0ead519a80 fix(a11y): add role=log aria-live to message timeline (P3-4)
Screen readers now announce new messages politely via role='log' +
aria-live='polite' on the message container in RoomTimeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:43:50 -04:00
jared 7d98b49a30 fix(a11y): add aria-label to exit-formatting and pin-menu buttons (P3-4)
Two icon-adjacent buttons were missing descriptive labels: the
"Exit formatting" key-symbol button in Toolbar.tsx and the "Pinned
messages" pin icon in RoomViewHeader.tsx.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 18:41:20 -04:00
8 changed files with 77 additions and 33 deletions
+4 -3
View File
@@ -35,10 +35,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
### 4. DM and Group Message Calls ### 4. DM and Group Message Calls
- **File:** `cinny/src/app/components/CallEmbedProvider.tsx` - **File:** `cinny/src/app/components/CallEmbedProvider.tsx`
- **Status:** **OPEN** - **Status:** **PARTIALLY FIXED ⚠️ UNTESTED** — Volume control added. Remaining: ringtone selection, suppression during active calls.
- **Issue:** Incoming call ringtone is hardcoded, lacks volume control, and is suppressed if the user is already in an active call. - **Issue:** Incoming call ringtone is hardcoded, lacks volume control, and is suppressed if the user is already in an active call.
- **Root Cause:** Ringing logic is tightly coupled to `RTCNotification` events in `CallEmbedProvider.tsx`, using a hardcoded audio file path. It lacks an abstraction for sound management or user-configurable settings for ringtones/volumes. - **Root Cause:** Ringing logic is tightly coupled to `RTCNotification` events in `CallEmbedProvider.tsx`, using a hardcoded audio file path. It lacks an abstraction for sound management or user-configurable settings for ringtones/volumes.
- **Proposed Fix:** Migrate sound asset management to a dedicated audio service. Implement user-configurable settings for ringtone and notification volume. Update the `IncomingCallListener` to support ringing even during active calls (if appropriate) by enhancing event handling. - **Fix Applied:** Added `ringtoneVolume` setting (0100, default 70). `IncomingCall` reads this setting and applies `audioElement.volume = ringtoneVolume / 100` before `play()`. Slider added to Settings → General → Calls section.
- **Remaining:** (a) Ringtone selection (still hardcoded to `call.ogg`); (b) Suppression during active calls — not investigated.
### 5. Seasonal Themes and Chat Backgrounds Design ### 5. Seasonal Themes and Chat Backgrounds Design
@@ -184,7 +185,7 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
| Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN | | Performance | Numerous event handlers (e.g., handleUserClick, handleReplyClick) lack `useCallback`, leading to unnecessary re-renders of message components. | `cinny/src/app/features/room/RoomTimeline.tsx` | OPEN |
| Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN | | Performance | The `submit` function and file handling callbacks (e.g., handleSendUpload) are re-created on every render, causing re-renders of the editor and toolbar components. | `cinny/src/app/features/room/RoomInput.tsx` | OPEN |
| Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View edit history"` | | Accessibility | `button` for edit history lacks `aria-label` | `cinny/src/app/components/message/content/FallbackContent.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View edit history"` |
| Accessibility | `button` for reaction lacks `aria-label` | `cinny/src/app/components/message/Reaction.tsx` | OPEN — emoji content is already screen-reader-accessible via alt text; parent caller would need to set aria-label per reaction | | Accessibility | `button` for reaction lacks `aria-label` | `cinny/src/app/components/message/Reaction.tsx` | **FIXED ⚠️ UNTESTED**`Reaction` component now computes `aria-label="{shortcode} reaction, N people"` internally using `getShortcodeFor`; custom (mxc://) emoji falls back to "custom emoji reaction". |
| Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View thread"` | | Accessibility | `button` for ThreadIndicator lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="View thread"` |
| Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="Jump to original message"` | | Accessibility | `button` for ReplyLayout lacks `aria-label` | `cinny/src/app/components/message/Reply.tsx` | FIXED ⚠️ UNTESTED — added `aria-label="Jump to original message"` |
+5 -2
View File
@@ -104,6 +104,7 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
const { room } = info; const { room } = info;
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const [ringtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume');
const roomName = useRoomName(room); const roomName = useRoomName(room);
const roomAvatar = useRoomAvatar(room, dm); const roomAvatar = useRoomAvatar(room, dm);
@@ -126,8 +127,10 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr
const playSound = useCallback(() => { const playSound = useCallback(() => {
const audioElement = audioRef.current; const audioElement = audioRef.current;
audioElement?.play().catch(() => undefined); if (!audioElement) return;
}, []); audioElement.volume = Math.max(0, Math.min(1, ringtoneVolume / 100));
audioElement.play().catch(() => undefined);
}, [ringtoneVolume]);
useEffect(() => { useEffect(() => {
const audioEl = audioRef.current; const audioEl = audioRef.current;
+1
View File
@@ -252,6 +252,7 @@ export function ExitFormatting({ tooltip }: ExitFormattingProps) {
onClick={handleClick} onClick={handleClick}
size="400" size="400"
radii="300" radii="300"
aria-label="Exit formatting"
> >
<Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text> <Text size="B400">{`Exit ${KeySymbol.Hyper}`}</Text>
</IconButton> </IconButton>
+36 -28
View File
@@ -15,34 +15,42 @@ export const Reaction = as<
reaction: string; reaction: string;
useAuthentication?: boolean; useAuthentication?: boolean;
} }
>(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => ( >(({ className, mx, count, reaction, useAuthentication, ...props }, ref) => {
<Box const shortcode = reaction.startsWith('mxc://')
as="button" ? 'custom emoji'
className={classNames(css.Reaction, className)} : (getShortcodeFor(getHexcodeForEmoji(reaction)) ?? reaction);
alignItems="Center" const label = `${shortcode} reaction, ${count} ${count === 1 ? 'person' : 'people'}`;
shrink="No"
gap="200" return (
{...props} <Box
ref={ref} as="button"
> className={classNames(css.Reaction, className)}
<Text className={css.ReactionText} as="span" size="T400"> alignItems="Center"
{reaction.startsWith('mxc://') ? ( shrink="No"
<img gap="200"
className={css.ReactionImg} aria-label={label}
src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction} {...props}
alt={reaction} ref={ref}
/> >
) : ( <Text className={css.ReactionText} as="span" size="T400">
<Text as="span" size="Inherit" truncate> {reaction.startsWith('mxc://') ? (
{reaction} <img
</Text> className={css.ReactionImg}
)} src={mxcUrlToHttp(mx, reaction, useAuthentication) ?? reaction}
</Text> alt={reaction}
<Text as="span" size="T300"> />
{count} ) : (
</Text> <Text as="span" size="Inherit" truncate>
</Box> {reaction}
)); </Text>
)}
</Text>
<Text as="span" size="T300">
{count}
</Text>
</Box>
);
});
type ReactionTooltipMsgProps = { type ReactionTooltipMsgProps = {
room: Room; room: Room;
+3
View File
@@ -2102,6 +2102,9 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli
<Box <Box
direction="Column" direction="Column"
justifyContent="End" justifyContent="End"
role="log"
aria-label="Message timeline"
aria-live="polite"
style={{ minHeight: '100%', padding: `${config.space.S600} 0` }} style={{ minHeight: '100%', padding: `${config.space.S600} 0` }}
> >
{!canPaginateBack && rangeAtStart && getItems().length > 0 && ( {!canPaginateBack && rangeAtStart && getItems().length > 0 && (
+2
View File
@@ -524,6 +524,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
<PageHeader <PageHeader
className={ContainerColor({ variant: 'Surface' })} className={ContainerColor({ variant: 'Surface' })}
balance={screenSize === ScreenSize.Mobile} balance={screenSize === ScreenSize.Mobile}
aria-label={`${name} room header`}
> >
<Box grow="Yes" gap="300"> <Box grow="Yes" gap="300">
{screenSize === ScreenSize.Mobile && ( {screenSize === ScreenSize.Mobile && (
@@ -652,6 +653,7 @@ export function RoomViewHeader({ callView }: { callView?: boolean }) {
style={{ position: 'relative' }} style={{ position: 'relative' }}
onClick={handleOpenPinMenu} onClick={handleOpenPinMenu}
ref={triggerRef} ref={triggerRef}
aria-label="Pinned messages"
aria-pressed={!!pinMenuAnchor} aria-pressed={!!pinMenuAnchor}
> >
{pinnedEvents.length > 0 && ( {pinnedEvents.length > 0 && (
@@ -1234,6 +1234,7 @@ function Calls() {
settingsAtom, settingsAtom,
'callJoinLeaveSound', 'callJoinLeaveSound',
); );
const [ringtoneVolume, setRingtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume');
const handleJoinLeaveSoundChange = (value: 'off' | 'chime' | 'soft' | 'retro') => { const handleJoinLeaveSoundChange = (value: 'off' | 'chime' | 'soft' | 'retro') => {
setCallJoinLeaveSound(value); setCallJoinLeaveSound(value);
@@ -1565,6 +1566,29 @@ function Calls() {
/> />
)} )}
</SequenceCard> </SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Ringtone Volume"
description="Volume of the incoming call ringtone."
after={
<Box direction="Row" alignItems="Center" gap="200" style={{ minWidth: '160px' }}>
<input
type="range"
min="0"
max="100"
step="5"
value={ringtoneVolume}
onChange={(e) => setRingtoneVolume(parseInt(e.target.value, 10))}
aria-label="Ringtone volume"
style={{ flex: 1, accentColor: 'var(--accent-orange)' }}
/>
<Text size="T200" style={{ minWidth: '32px', textAlign: 'right' }}>
{ringtoneVolume}%
</Text>
</Box>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column"> <SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile <SettingTile
title="Join & Leave Sounds" title="Join & Leave Sounds"
+2
View File
@@ -148,6 +148,7 @@ export interface Settings {
afkTimeoutMinutes: number; afkTimeoutMinutes: number;
callJoinLeaveSound: 'off' | 'chime' | 'soft' | 'retro'; callJoinLeaveSound: 'off' | 'chime' | 'soft' | 'retro';
ringtoneVolume: number; // 0100
seasonalThemeOverride: seasonalThemeOverride:
| 'auto' | 'auto'
@@ -242,6 +243,7 @@ const defaultSettings: Settings = {
afkTimeoutMinutes: 10, afkTimeoutMinutes: 10,
callJoinLeaveSound: 'chime', callJoinLeaveSound: 'chime',
ringtoneVolume: 70,
seasonalThemeOverride: 'auto', seasonalThemeOverride: 'auto',
}; };