feat: P2-3 sort rooms, P2-5 quiet hours, P2-2 custom notification sounds
P2-3 — Sort Non-Space Rooms: - homeRoomSort: 'recent' | 'alpha' | 'unread' setting (default 'recent') - factoryRoomIdByUnread comparator: unread rooms first, tie-break by count - Sort icon button in Rooms NavCategoryHeader opens PopOut menu with three options (Recent Activity / A→Z / Unread First), checkmark on active - Collapsed state still filters to unread-only regardless of sort choice P2-5 — Notification Quiet Hours: - quietHoursEnabled / quietHoursStart / quietHoursEnd added to settings (defaults: false, '23:00', '08:00') - isInQuietHours() helper handles both normal and overnight spans; start===end treated as zero-length window (disabled) to avoid silent no-op - Both InviteNotifications and MessageNotifications gate notify() and playSound() behind the quiet-hours check - Settings → Notifications: new Quiet Hours card with Switch + two <input type="time"> fields (only shown when enabled) P2-2 — Custom Notification Sounds: - messageSoundId / inviteSoundId settings: 'notification'|'invite'|'call'|'none' - notificationSounds.ts: shared NOTIFICATION_SOUND_MAP (removes duplication between ClientNonUIFeatures and SystemNotification — code review fix) - Audio source updated reactively via useEffect when sound ID changes - Settings → Notifications: Message Sound + Invite Sound selects expand when the master sound toggle is on; each has a ▶ preview button - playPreview() catches audio.play() rejections (code review fix) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import LogoHighlightSVG from '../../../../public/res/lotus-highlight.png';
|
||||
import NotificationSound from '../../../../public/sound/notification.ogg';
|
||||
import InviteSound from '../../../../public/sound/invite.ogg';
|
||||
import { notificationPermission, setFavicon } from '../../utils/dom';
|
||||
import { NOTIFICATION_SOUND_MAP } from '../../utils/notificationSounds';
|
||||
import { useSetting } from '../../state/hooks/settings';
|
||||
import { settingsAtom } from '../../state/settings';
|
||||
import { allInvitesAtom } from '../../state/room-list/inviteList';
|
||||
@@ -28,6 +29,18 @@ import { useInboxNotificationsSelected } from '../../hooks/router/useInbox';
|
||||
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
|
||||
import { usePresenceUpdater } from '../../hooks/usePresenceUpdater';
|
||||
|
||||
function isInQuietHours(start: string, end: string): boolean {
|
||||
const now = new Date();
|
||||
const [sh, sm] = start.split(':').map(Number);
|
||||
const [eh, em] = end.split(':').map(Number);
|
||||
const cur = now.getHours() * 60 + now.getMinutes();
|
||||
const s = sh! * 60 + (sm ?? 0);
|
||||
const e = eh! * 60 + (em ?? 0);
|
||||
// start===end means zero-length window → treat as disabled (no quiet hours)
|
||||
if (s === e) return false;
|
||||
return s < e ? cur >= s && cur < e : cur >= s || cur < e;
|
||||
}
|
||||
|
||||
function SystemEmojiFeature() {
|
||||
const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
|
||||
|
||||
@@ -90,6 +103,23 @@ function InviteNotifications() {
|
||||
const navigate = useNavigate();
|
||||
const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
|
||||
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
|
||||
const [quietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled');
|
||||
const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
|
||||
const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
|
||||
const [inviteSoundId] = useSetting(settingsAtom, 'inviteSoundId');
|
||||
|
||||
const soundSrc =
|
||||
inviteSoundId !== 'none' ? (NOTIFICATION_SOUND_MAP[inviteSoundId] ?? InviteSound) : null;
|
||||
|
||||
useEffect(() => {
|
||||
const el = audioRef.current;
|
||||
if (!el) return;
|
||||
const source = el.querySelector('source');
|
||||
if (source && soundSrc) {
|
||||
source.src = soundSrc;
|
||||
el.load();
|
||||
}
|
||||
}, [soundSrc]);
|
||||
|
||||
const notify = useCallback(
|
||||
(count: number) => {
|
||||
@@ -115,19 +145,34 @@ function InviteNotifications() {
|
||||
|
||||
useEffect(() => {
|
||||
if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') {
|
||||
if (showNotifications && notificationPermission('granted')) {
|
||||
notify(invites.length - perviousInviteLen);
|
||||
}
|
||||
const quietActive = quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd);
|
||||
if (!quietActive) {
|
||||
if (showNotifications && notificationPermission('granted')) {
|
||||
notify(invites.length - perviousInviteLen);
|
||||
}
|
||||
|
||||
if (notificationSound) {
|
||||
playSound();
|
||||
if (notificationSound && inviteSoundId !== 'none') {
|
||||
playSound();
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [mx, invites, perviousInviteLen, showNotifications, notificationSound, notify, playSound]);
|
||||
}, [
|
||||
mx,
|
||||
invites,
|
||||
perviousInviteLen,
|
||||
showNotifications,
|
||||
notificationSound,
|
||||
notify,
|
||||
playSound,
|
||||
quietHoursEnabled,
|
||||
quietHoursStart,
|
||||
quietHoursEnd,
|
||||
inviteSoundId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<audio ref={audioRef} style={{ display: 'none' }}>
|
||||
<source src={InviteSound} type="audio/ogg" />
|
||||
<source src={soundSrc ?? InviteSound} type="audio/ogg" />
|
||||
</audio>
|
||||
);
|
||||
}
|
||||
@@ -145,6 +190,25 @@ function MessageNotifications() {
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const [showNotifications] = useSetting(settingsAtom, 'showNotifications');
|
||||
const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds');
|
||||
const [quietHoursEnabled] = useSetting(settingsAtom, 'quietHoursEnabled');
|
||||
const [quietHoursStart] = useSetting(settingsAtom, 'quietHoursStart');
|
||||
const [quietHoursEnd] = useSetting(settingsAtom, 'quietHoursEnd');
|
||||
const [messageSoundId] = useSetting(settingsAtom, 'messageSoundId');
|
||||
|
||||
const soundSrc =
|
||||
messageSoundId !== 'none'
|
||||
? (NOTIFICATION_SOUND_MAP[messageSoundId] ?? NotificationSound)
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
const el = audioRef.current;
|
||||
if (!el) return;
|
||||
const source = el.querySelector('source');
|
||||
if (source && soundSrc) {
|
||||
source.src = soundSrc;
|
||||
el.load();
|
||||
}
|
||||
}, [soundSrc]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const notificationSelected = useInboxNotificationsSelected();
|
||||
@@ -221,22 +285,25 @@ function MessageNotifications() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (showNotifications && notificationPermission('granted')) {
|
||||
const avatarMxc =
|
||||
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
|
||||
notify({
|
||||
roomName: room.name ?? 'Unknown',
|
||||
roomAvatar: avatarMxc
|
||||
? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined)
|
||||
: undefined,
|
||||
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
|
||||
roomId: room.roomId,
|
||||
eventId,
|
||||
});
|
||||
}
|
||||
const quietActive = quietHoursEnabled && isInQuietHours(quietHoursStart, quietHoursEnd);
|
||||
if (!quietActive) {
|
||||
if (showNotifications && notificationPermission('granted')) {
|
||||
const avatarMxc =
|
||||
room.getAvatarFallbackMember()?.getMxcAvatarUrl() ?? room.getMxcAvatarUrl();
|
||||
notify({
|
||||
roomName: room.name ?? 'Unknown',
|
||||
roomAvatar: avatarMxc
|
||||
? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 96, 96, 'crop') ?? undefined)
|
||||
: undefined,
|
||||
username: getMemberDisplayName(room, sender) ?? getMxIdLocalPart(sender) ?? sender,
|
||||
roomId: room.roomId,
|
||||
eventId,
|
||||
});
|
||||
}
|
||||
|
||||
if (notificationSound) {
|
||||
playSound();
|
||||
if (notificationSound && messageSoundId !== 'none') {
|
||||
playSound();
|
||||
}
|
||||
}
|
||||
};
|
||||
mx.on(RoomEvent.Timeline, handleTimelineEvent);
|
||||
@@ -252,11 +319,15 @@ function MessageNotifications() {
|
||||
notify,
|
||||
selectedRoomId,
|
||||
useAuthentication,
|
||||
quietHoursEnabled,
|
||||
quietHoursStart,
|
||||
quietHoursEnd,
|
||||
messageSoundId,
|
||||
]);
|
||||
|
||||
return (
|
||||
<audio ref={audioRef} style={{ display: 'none' }}>
|
||||
<source src={NotificationSound} type="audio/ogg" />
|
||||
<source src={soundSrc ?? NotificationSound} type="audio/ogg" />
|
||||
</audio>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user