diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 36101fb..9d5a27d 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -61,4 +61,12 @@ module.exports = { "@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-shadow": "error" }, + overrides: [ + { + files: ['*.ts'], + rules: { + 'no-undef': 'off', + }, + }, + ], }; diff --git a/src/app/hooks/usePermission.js b/src/app/hooks/usePermission.js deleted file mode 100644 index 5dc7607..0000000 --- a/src/app/hooks/usePermission.js +++ /dev/null @@ -1,28 +0,0 @@ -/* eslint-disable import/prefer-default-export */ - -import { useEffect, useState } from 'react'; - -export function usePermission(name, initial) { - const [state, setState] = useState(initial); - - useEffect(() => { - let descriptor; - - const update = () => setState(descriptor.state); - - if (navigator.permissions?.query) { - navigator.permissions.query({ name }).then((_descriptor) => { - descriptor = _descriptor; - - update(); - descriptor.addEventListener('change', update); - }); - } - - return () => { - if (descriptor) descriptor.removeEventListener('change', update); - }; - }, []); - - return [state, setState]; -} diff --git a/src/app/hooks/usePermission.ts b/src/app/hooks/usePermission.ts new file mode 100644 index 0000000..5a3ec9f --- /dev/null +++ b/src/app/hooks/usePermission.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from "react"; + +export function usePermissionState(name: PermissionName, initialValue: PermissionState = 'prompt') { + const [permissionState, setPermissionState] = useState(initialValue); + + useEffect(() => { + let permissionStatus: PermissionStatus; + + function handlePermissionChange(this: PermissionStatus) { + setPermissionState(this.state); + } + + navigator.permissions + .query({ name }) + .then((permStatus: PermissionStatus) => { + permissionStatus = permStatus; + handlePermissionChange.apply(permStatus); + permStatus.addEventListener("change", handlePermissionChange); + }) + .catch(() => { + // Silence error since FF doesn't support microphone permission + }); + + return () => { + permissionStatus?.removeEventListener("change", handlePermissionChange); + }; + }, [name]); + + return permissionState; +} \ No newline at end of file diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index f2951b8..779931d 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -7,9 +7,8 @@ import settings from '../../../client/state/settings'; import navigation from '../../../client/state/navigation'; import { toggleSystemTheme, - toggleNotifications, toggleNotificationSounds, } from '../../../client/action/settings'; -import { usePermission } from '../../hooks/usePermission'; +import { usePermissionState } from '../../hooks/usePermission'; import Text from '../../atoms/text/Text'; import IconButton from '../../atoms/button/IconButton'; @@ -230,23 +229,25 @@ function AppearanceSection() { } function NotificationsSection() { - const [permission, setPermission] = usePermission('notifications', window.Notification?.permission); - - const [, updateState] = useState({}); + const notifPermission = usePermissionState('notifications', window.Notification?.permission ?? "denied"); + const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications') + const [isNotificationSounds, setIsNotificationSounds] = useSetting(settingsAtom, 'isNotificationSounds') const renderOptions = () => { if (window.Notification === undefined) { return Not supported in this browser.; } - if (permission === 'granted') { + if (notifPermission === 'denied') { + return Permission Denied + } + + if (notifPermission === 'granted') { return ( { - toggleNotifications(); - setPermission(window.Notification?.permission); - updateState({}); + setShowNotifications(!showNotifications); }} /> ); @@ -255,7 +256,9 @@ function NotificationsSection() { return ( @@ -275,8 +278,8 @@ function NotificationsSection() { title="Notification Sound" options={( { toggleNotificationSounds(); updateState({}); }} + isActive={isNotificationSounds} + onToggle={() => setIsNotificationSounds(!isNotificationSounds)} /> )} content={Play sound when new messages arrive.} diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 947764c..5f557aa 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -58,6 +58,7 @@ function InviteNotifications() { const mx = useMatrixClient(); const navigate = useNavigate(); + const [showNotifications] = useSetting(settingsAtom, 'showNotifications'); const [notificationSound] = useSetting(settingsAtom, 'isNotificationSounds'); const notify = useCallback( @@ -84,7 +85,7 @@ function InviteNotifications() { useEffect(() => { if (invites.length > perviousInviteLen && mx.getSyncState() === 'SYNCING') { - if (Notification.permission === 'granted') { + if (showNotifications && Notification.permission === 'granted') { notify(invites.length - perviousInviteLen); } @@ -92,7 +93,7 @@ function InviteNotifications() { playSound(); } } - }, [mx, invites, perviousInviteLen, notificationSound, notify, playSound]); + }, [mx, invites, perviousInviteLen, showNotifications, notificationSound, notify, playSound]); return ( // eslint-disable-next-line jsx-a11y/media-has-caption diff --git a/src/app/state/room/roomToUnread.ts b/src/app/state/room/roomToUnread.ts index 5a00940..8cb9d95 100644 --- a/src/app/state/room/roomToUnread.ts +++ b/src/app/state/room/roomToUnread.ts @@ -185,8 +185,11 @@ export const useBindRoomToUnreadAtom = ( useSyncState( mx, useCallback( - (state) => { - if (state === SyncState.Prepared) { + (state, prevState) => { + if ( + (state === SyncState.Prepared && prevState === null) || + (state === SyncState.Syncing && prevState !== SyncState.Syncing) + ) { setUnreadAtom({ type: 'RESET', unreadInfos: getUnreadInfos(mx), diff --git a/src/client/action/settings.js b/src/client/action/settings.js index 7b539c8..e849702 100644 --- a/src/client/action/settings.js +++ b/src/client/action/settings.js @@ -30,15 +30,3 @@ export function toggleNickAvatarEvents() { type: cons.actions.settings.TOGGLE_NICKAVATAR_EVENT, }); } - -export function toggleNotifications() { - appDispatcher.dispatch({ - type: cons.actions.settings.TOGGLE_NOTIFICATIONS, - }); -} - -export function toggleNotificationSounds() { - appDispatcher.dispatch({ - type: cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS, - }); -} diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 523e871..353ce47 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -52,8 +52,6 @@ const cons = { TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER', TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT', TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT', - TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS', - TOGGLE_NOTIFICATION_SOUNDS: 'TOGGLE_NOTIFICATION_SOUNDS', }, }, events: { @@ -81,8 +79,6 @@ const cons = { PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED', MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED', NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED', - NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED', - NOTIFICATION_SOUNDS_TOGGLED: 'NOTIFICATION_SOUNDS_TOGGLED', }, }, }; diff --git a/src/client/state/settings.js b/src/client/state/settings.js index d39b2ca..bf9562c 100644 --- a/src/client/state/settings.js +++ b/src/client/state/settings.js @@ -33,8 +33,6 @@ class Settings extends EventEmitter { this.isPeopleDrawer = this.getIsPeopleDrawer(); this.hideMembershipEvents = this.getHideMembershipEvents(); this.hideNickAvatarEvents = this.getHideNickAvatarEvents(); - this._showNotifications = this.getShowNotifications(); - this.isNotificationSounds = this.getIsNotificationSounds(); this.darkModeQueryList = window.matchMedia('(prefers-color-scheme: dark)'); @@ -137,29 +135,6 @@ class Settings extends EventEmitter { return settings.isPeopleDrawer; } - get showNotifications() { - if (window.Notification?.permission !== 'granted') return false; - return this._showNotifications; - } - - getShowNotifications() { - if (typeof this._showNotifications === 'boolean') return this._showNotifications; - - const settings = getSettings(); - if (settings === null) return true; - if (typeof settings.showNotifications === 'undefined') return true; - return settings.showNotifications; - } - - getIsNotificationSounds() { - if (typeof this.isNotificationSounds === 'boolean') return this.isNotificationSounds; - - const settings = getSettings(); - if (settings === null) return true; - if (typeof settings.isNotificationSounds === 'undefined') return true; - return settings.isNotificationSounds; - } - setter(action) { const actions = { [cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => { @@ -185,20 +160,6 @@ class Settings extends EventEmitter { setSettings('hideNickAvatarEvents', this.hideNickAvatarEvents); this.emit(cons.events.settings.NICKAVATAR_EVENTS_TOGGLED, this.hideNickAvatarEvents); }, - [cons.actions.settings.TOGGLE_NOTIFICATIONS]: async () => { - if (window.Notification?.permission !== 'granted') { - this._showNotifications = false; - } else { - this._showNotifications = !this._showNotifications; - } - setSettings('showNotifications', this._showNotifications); - this.emit(cons.events.settings.NOTIFICATIONS_TOGGLED, this._showNotifications); - }, - [cons.actions.settings.TOGGLE_NOTIFICATION_SOUNDS]: () => { - this.isNotificationSounds = !this.isNotificationSounds; - setSettings('isNotificationSounds', this.isNotificationSounds); - this.emit(cons.events.settings.NOTIFICATION_SOUNDS_TOGGLED, this.isNotificationSounds); - }, }; actions[action.type]?.();