feat(call): add sound effects and incoming call notification UI

- Implemented sound management for call events including mute, unmute, deafen, undeafen, call joined, call depart, and dialing sounds.
- Created a new CallSounds class to handle sound playback and state management.
- Added IncomingCallNotification component to display incoming call alerts with caller information and action buttons.
- Styled the incoming call notification using vanilla-extract CSS for animations and layout.
- Integrated sound playback for incoming calls with a 15-second threshold for dialing sound.
This commit is contained in:
2026-02-05 03:24:18 +11:00
parent cd912025f1
commit 1a452f52ca
27 changed files with 1888 additions and 179 deletions

View File

@@ -32,7 +32,7 @@ import FocusTrap from 'focus-trap-react';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings';
import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
import { DateFormat, EmojiStyle, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile';
import { KeySymbol } from '../../../utils/key-symbol';
import { isMacOS } from '../../../utils/user-agent';
@@ -303,10 +303,50 @@ function PageZoomInput() {
);
}
const emojiStyleNames: Record<EmojiStyle, string> = {
[EmojiStyle.System]: 'System',
[EmojiStyle.Apple]: 'Apple',
[EmojiStyle.Twemoji]: 'Twemoji',
};
type EmojiStyleSelectorProps = {
selected: EmojiStyle;
onSelect: (style: EmojiStyle) => void;
};
const EmojiStyleSelector = as<'div', EmojiStyleSelectorProps>(
({ selected, onSelect, ...props }, ref) => (
<Menu {...props} ref={ref}>
<Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
{Object.values(EmojiStyle).map((style) => (
<MenuItem
key={style}
size="300"
variant={style === selected ? 'Primary' : 'Surface'}
radii="300"
onClick={() => onSelect(style)}
>
<Text size="T300">{emojiStyleNames[style]}</Text>
</MenuItem>
))}
</Box>
</Menu>
)
);
function Appearance() {
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
const [emojiStyle, setEmojiStyle] = useSetting(settingsAtom, 'emojiStyle');
const [emojiMenuCords, setEmojiMenuCords] = useState<RectCords>();
const handleEmojiMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setEmojiMenuCords(evt.currentTarget.getBoundingClientRect());
};
const handleEmojiStyleSelect = (style: EmojiStyle) => {
setEmojiStyle(style);
setEmojiMenuCords(undefined);
};
return (
<Box direction="Column" gap="100">
@@ -342,8 +382,45 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Twitter Emoji"
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
title="Emoji Style"
description="Choose which emoji set to display."
after={
<>
<Button
size="300"
variant="Primary"
outlined
fill="Soft"
radii="300"
after={<Icon size="300" src={Icons.ChevronBottom} />}
onClick={handleEmojiMenu}
>
<Text size="T300">{emojiStyleNames[emojiStyle]}</Text>
</Button>
<PopOut
anchor={emojiMenuCords}
offset={5}
position="Bottom"
align="End"
content={
<FocusTrap
focusTrapOptions={{
initialFocus: false,
onDeactivate: () => setEmojiMenuCords(undefined),
clickOutsideDeactivates: true,
isKeyForward: (evt: KeyboardEvent) =>
evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
isKeyBackward: (evt: KeyboardEvent) =>
evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
escapeDeactivates: stopPropagation,
}}
>
<EmojiStyleSelector selected={emojiStyle} onSelect={handleEmojiStyleSelect} />
</FocusTrap>
}
/>
</>
}
/>
</SequenceCard>