--- /dev/null
+/* 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];
+}
import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings';
-import { toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents } from '../../../client/action/settings';
+import {
+ toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
+ toggleNotifications,
+} from '../../../client/action/settings';
import logout from '../../../client/action/logout';
+import { usePermission } from '../../hooks/usePermission';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
import SunIC from '../../../../public/res/ic/outlined/sun.svg';
import LockIC from '../../../../public/res/ic/outlined/lock.svg';
+import BellIC from '../../../../public/res/ic/outlined/bell.svg';
import InfoIC from '../../../../public/res/ic/outlined/info.svg';
import PowerIC from '../../../../public/res/ic/outlined/power.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
/>
{(() => {
if (!settings.useSystemTheme) {
- return <SettingTile
- title="Theme"
- content={(
- <SegmentedControls
- selected={settings.getThemeIndex()}
- segments={[
- { text: 'Light' },
- { text: 'Silver' },
- { text: 'Dark' },
- { text: 'Butter' },
- ]}
- onSelect={(index) => settings.setTheme(index)}
- />
+ return (
+ <SettingTile
+ title="Theme"
+ content={(
+ <SegmentedControls
+ selected={settings.getThemeIndex()}
+ segments={[
+ { text: 'Light' },
+ { text: 'Silver' },
+ { text: 'Dark' },
+ { text: 'Butter' },
+ ]}
+ onSelect={(index) => settings.setTheme(index)}
+ />
)}
- />
+ />
+ );
}
})()}
<SettingTile
);
}
+function NotificationsSection() {
+ const [permission, setPermission] = usePermission('notifications', window.Notification?.permission);
+
+ const [, updateState] = useState({});
+
+ const renderOptions = () => {
+ if (window.Notification === undefined) {
+ return <Text className="set-notifications__not-supported">Not supported in this browser.</Text>;
+ }
+
+ if (permission === 'granted') {
+ return (
+ <Toggle
+ isActive={settings._showNotifications}
+ onToggle={() => {
+ toggleNotifications();
+ setPermission(window.Notification?.permission);
+ updateState({});
+ }}
+ />
+ );
+ }
+
+ return (
+ <Button
+ variant="primary"
+ onClick={() => window.Notification.requestPermission().then(setPermission)}
+ >
+ Request permission
+ </Button>
+ );
+ };
+
+ return (
+ <div className="set-notifications settings-content">
+ <SettingTile
+ title="Show desktop notifications"
+ options={renderOptions()}
+ content={<Text variant="b3">Show notifications when new messages arrive.</Text>}
+ />
+ </div>
+ );
+}
+
function SecuritySection() {
return (
<div className="set-security settings-content">
render() {
return <AppearanceSection />;
},
+ }, {
+ name: 'Notifications',
+ iconSrc: BellIC,
+ render() {
+ return <NotificationsSection />;
+ },
}, {
name: 'Security & Privacy',
iconSrc: LockIC,
}
}
+.set-notifications {
+ &__not-supported {
+ padding: 0 var(--sp-ultra-tight);
+ }
+}
+
.set-about {
&__branding {
margin-top: var(--sp-extra-tight);
type: cons.actions.settings.TOGGLE_NICKAVATAR_EVENT,
});
}
+
+export function toggleNotifications() {
+ appDispatcher.dispatch({
+ type: cons.actions.settings.TOGGLE_NOTIFICATIONS,
+ });
+}
import EventEmitter from 'events';
+import { selectRoom } from '../action/navigation';
import cons from './cons';
+import navigation from './navigation';
+import settings from './settings';
function isNotifEvent(mEvent) {
const eType = mEvent.getType();
this._initNoti();
this._listenEvents();
+ // Ask for permission by default after loading
+ window.Notification?.requestPermission();
+
// TODO:
window.notifications = this;
}
[...parentIds].forEach((parentId) => this._deleteNoti(parentId, total, highlight, roomId));
}
+ async _displayPopupNoti(mEvent, room) {
+ if (!settings.showNotifications) return;
+
+ const actions = this.matrixClient.getPushActionsForEvent(mEvent);
+ if (!actions?.notify) return;
+
+ if (navigation.selectedRoomId === room.roomId && document.visibilityState === 'visible') return;
+
+ if (mEvent.isEncrypted()) {
+ await mEvent.attemptDecryption(this.matrixClient.crypto);
+ }
+
+ let title;
+ if (!mEvent.sender || room.name === mEvent.sender.name) {
+ title = room.name;
+ } else if (mEvent.sender) {
+ title = `${mEvent.sender.name} (${room.name})`;
+ }
+
+ const noti = new window.Notification(title, {
+ body: mEvent.getContent().body,
+ icon: mEvent.sender?.getAvatarUrl(this.matrixClient.baseUrl, 36, 36, 'crop'),
+ });
+ noti.onclick = () => selectRoom(room.roomId, mEvent.getId());
+ }
+
_listenEvents() {
this.matrixClient.on('Room.timeline', (mEvent, room) => {
if (!isNotifEvent(mEvent)) return;
const noti = this.getNoti(room.roomId);
this._setNoti(room.roomId, total - noti.total, highlight - noti.highlight);
+
+ if (this.matrixClient.getSyncState() === 'SYNCING') {
+ this._displayPopupNoti(mEvent, room);
+ }
});
this.matrixClient.on('Room.receipt', (mEvent, room) => {
TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER',
TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT',
TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
+ TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS',
},
},
events: {
PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED',
MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED',
NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
+ NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED',
},
},
};
this.isPeopleDrawer = this.getIsPeopleDrawer();
this.hideMembershipEvents = this.getHideMembershipEvents();
this.hideNickAvatarEvents = this.getHideNickAvatarEvents();
+ this._showNotifications = this.getShowNotifications();
this.isTouchScreenDevice = ('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0);
}
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;
+ }
+
setter(action) {
const actions = {
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
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);
+ },
};
actions[action.type]?.();