Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Desktop notifications #252

Merged
merged 12 commits into from
Jan 29, 2022
28 changes: 28 additions & 0 deletions src/app/hooks/usePermission.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/* 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];
}
87 changes: 72 additions & 15 deletions src/app/organisms/settings/Settings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import './Settings.scss';
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';
Expand All @@ -24,6 +28,7 @@ import ProfileEditor from '../profile-editor/ProfileEditor';
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';
Expand Down Expand Up @@ -60,21 +65,23 @@ function AppearanceSection() {
/>
{(() => {
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
Expand Down Expand Up @@ -111,6 +118,50 @@ function AppearanceSection() {
);
}

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">
Expand Down Expand Up @@ -178,6 +229,12 @@ function Settings({ isOpen, onRequestClose }) {
render() {
return <AppearanceSection />;
},
}, {
name: 'Notifications',
iconSrc: BellIC,
render() {
return <NotificationsSection />;
},
}, {
name: 'Security & Privacy',
iconSrc: LockIC,
Expand Down
6 changes: 6 additions & 0 deletions src/app/organisms/settings/Settings.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
}
}

.set-notifications {
&__not-supported {
padding: 0 var(--sp-ultra-tight);
}
}

.set-about {
&__branding {
margin-top: var(--sp-extra-tight);
Expand Down
6 changes: 6 additions & 0 deletions src/client/action/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,9 @@ export function toggleNickAvatarEvents() {
type: cons.actions.settings.TOGGLE_NICKAVATAR_EVENT,
});
}

export function toggleNotifications() {
appDispatcher.dispatch({
type: cons.actions.settings.TOGGLE_NOTIFICATIONS,
});
}
36 changes: 36 additions & 0 deletions src/client/state/Notifications.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
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();
Expand All @@ -24,6 +27,9 @@ class Notifications extends EventEmitter {
this._initNoti();
this._listenEvents();

// Ask for permission by default after loading
window.Notification?.requestPermission();

// TODO:
window.notifications = this;
}
Expand Down Expand Up @@ -158,6 +164,32 @@ class Notifications extends EventEmitter {
[...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;
Expand All @@ -172,6 +204,10 @@ class Notifications extends EventEmitter {

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) => {
Expand Down
2 changes: 2 additions & 0 deletions src/client/state/cons.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const cons = {
TOGGLE_PEOPLE_DRAWER: 'TOGGLE_PEOPLE_DRAWER',
TOGGLE_MEMBERSHIP_EVENT: 'TOGGLE_MEMBERSHIP_EVENT',
TOGGLE_NICKAVATAR_EVENT: 'TOGGLE_NICKAVATAR_EVENT',
TOGGLE_NOTIFICATIONS: 'TOGGLE_NOTIFICATIONS',
},
},
events: {
Expand Down Expand Up @@ -116,6 +117,7 @@ const cons = {
PEOPLE_DRAWER_TOGGLED: 'PEOPLE_DRAWER_TOGGLED',
MEMBERSHIP_EVENTS_TOGGLED: 'MEMBERSHIP_EVENTS_TOGGLED',
NICKAVATAR_EVENTS_TOGGLED: 'NICKAVATAR_EVENTS_TOGGLED',
NOTIFICATIONS_TOGGLED: 'NOTIFICATIONS_TOGGLED',
},
},
};
Expand Down
24 changes: 24 additions & 0 deletions src/client/state/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Settings extends EventEmitter {
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);
}
Expand Down Expand Up @@ -110,6 +111,20 @@ 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;
ajbura marked this conversation as resolved.
Show resolved Hide resolved
return settings.showNotifications;
}

setter(action) {
const actions = {
[cons.actions.settings.TOGGLE_SYSTEM_THEME]: () => {
Expand Down Expand Up @@ -140,6 +155,15 @@ 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);
},
};

actions[action.type]?.();
Expand Down