--- /dev/null
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2707_1961)">
+<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
+<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
+<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#45B83B"/>
+</g>
+<defs>
+<clipPath id="clip0_2707_1961">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
--- /dev/null
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2707_2015)">
+<path d="M10.5867 17.3522C10.0727 17.4492 9.54226 17.5 9 17.5C4.30558 17.5 0.5 13.6944 0.5 9C0.5 4.30558 4.30558 0.5 9 0.5C13.6944 0.5 17.5 4.30558 17.5 9C17.5 9.54226 17.4492 10.0727 17.3522 10.5867C16.6511 10.2123 15.8503 10 15 10C12.2386 10 10 12.2386 10 15C10 15.8503 10.2123 16.6511 10.5867 17.3522Z" fill="white"/>
+<path d="M10 6.39999C10 6.67614 9.77614 6.89999 9.5 6.89999C9.22386 6.89999 9 6.67614 9 6.39999C9 6.12385 9.22386 5.89999 9.5 5.89999C9.77614 5.89999 10 6.12385 10 6.39999Z" fill="black"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M9 0C4 0 0 4 0 9C0 14 4 18 9 18C9.63967 18 10.263 17.9345 10.8636 17.8099C10.3186 17.0091 10 16.0417 10 15C10 12.2386 12.2386 10 15 10C16.0417 10 17.0091 10.3186 17.8099 10.8636C17.9345 10.263 18 9.63967 18 9C18 4 14 0 9 0ZM1.2 10.8L4.7 8.5V8.2C4.7 6.4 6 5 7.8 4.8H8.2C9.4 4.8 10.5 5.4 11.1 6.4C11.4 6.3 11.7 6.3 12 6.3C12.4 6.3 12.8 6.3 13.2 6.4C13.9 6.6 14.6 6.9 15.2 7.3C14.6 7.1 14 7 13.3 7C12.1 7 11.1 7.4 10.4 8.4C9.7 9.3 9.3 10.4 9.3 11.6C9.3 13.1 8.9 14.5 8 15.8C7.93744 15.8834 7.87923 15.9625 7.82356 16.0381C7.6123 16.325 7.43739 16.5626 7.2 16.8C4.2 16.1 1.9 13.8 1.2 10.8Z" fill="black"/>
+<path d="M18 15C18 16.6569 16.6569 18 15 18C13.3431 18 12 16.6569 12 15C12 13.3431 13.3431 12 15 12C16.6569 12 18 13.3431 18 15Z" fill="#989898"/>
+</g>
+<defs>
+<clipPath id="clip0_2707_2015">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>
-import { avatarInitials } from '../../../util/common';
-
-function cssVar(name) {
- return getComputedStyle(document.body).getPropertyValue(name);
-}
+import { avatarInitials, cssVar } from '../../../util/common';
// renders the avatar and returns it as an URL
export default async function renderAvatar({
--- /dev/null
+/* eslint-disable import/prefer-default-export */
+import { useState, useEffect } from 'react';
+
+import initMatrix from '../../client/initMatrix';
+
+export function useAccountData(eventType) {
+ const mx = initMatrix.matrixClient;
+ const [event, setEvent] = useState(mx.getAccountData(eventType)?.getContent());
+
+ useEffect(() => {
+ const handleChange = (mEvent) => {
+ if (mEvent.getType() !== eventType) return;
+ setEvent(mEvent.getContent());
+ };
+ mx.on('accountData', handleChange);
+ return () => {
+ mx.removeListener('accountData', handleChange);
+ };
+ }, [eventType]);
+
+ return event;
+}
--- /dev/null
+import React from 'react';
+
+import initMatrix from '../../../client/initMatrix';
+import { openReusableContextMenu } from '../../../client/action/navigation';
+import { getEventCords } from '../../../util/common';
+
+import Text from '../../atoms/text/Text';
+import Button from '../../atoms/button/Button';
+import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
+import SettingTile from '../setting-tile/SettingTile';
+
+import NotificationSelector from './NotificationSelector';
+
+import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
+
+import { useAccountData } from '../../hooks/useAccountData';
+
+export const notifType = {
+ ON: 'on',
+ OFF: 'off',
+ NOISY: 'noisy',
+};
+export const typeToLabel = {
+ [notifType.ON]: 'On',
+ [notifType.OFF]: 'Off',
+ [notifType.NOISY]: 'Noisy',
+};
+Object.freeze(notifType);
+
+const DM = '.m.rule.room_one_to_one';
+const ENC_DM = '.m.rule.encrypted_room_one_to_one';
+const ROOM = '.m.rule.message';
+const ENC_ROOM = '.m.rule.encrypted';
+
+export function getActionType(rule) {
+ const { actions } = rule;
+ if (actions.find((action) => action?.set_tweak === 'sound')) return notifType.NOISY;
+ if (actions.find((action) => action?.set_tweak === 'highlight')) return notifType.ON;
+ if (actions.find((action) => action === 'dont_notify')) return notifType.OFF;
+ return notifType.OFF;
+}
+
+export function getTypeActions(type, highlightValue = false) {
+ if (type === notifType.OFF) return ['dont_notify'];
+
+ const highlight = { set_tweak: 'highlight' };
+ if (typeof highlightValue === 'boolean') highlight.value = highlightValue;
+ if (type === notifType.ON) return ['notify', highlight];
+
+ const sound = { set_tweak: 'sound', value: 'default' };
+ return ['notify', sound, highlight];
+}
+
+function useGlobalNotif() {
+ const mx = initMatrix.matrixClient;
+ const pushRules = useAccountData('m.push_rules');
+ const underride = pushRules?.global?.underride ?? [];
+ const rulesToType = {
+ [DM]: notifType.ON,
+ [ENC_DM]: notifType.ON,
+ [ROOM]: notifType.NOISY,
+ [ENC_ROOM]: notifType.NOISY,
+ };
+
+ const getRuleCondition = (rule) => {
+ const condition = [];
+ if (rule === DM || rule === ENC_DM) {
+ condition.push({ kind: 'room_member_count', is: '2' });
+ }
+ condition.push({
+ kind: 'event_match',
+ key: 'type',
+ pattern: [ENC_DM, ENC_ROOM].includes(rule) ? 'm.room.encrypted' : 'm.room.message',
+ });
+ return condition;
+ };
+
+ const setRule = (rule, type) => {
+ const content = pushRules ?? {};
+ if (!content.global) content.global = {};
+ if (!content.global.underride) content.global.underride = [];
+ const ur = content.global.underride;
+ let ruleContent = ur.find((action) => action?.rule_id === rule);
+ if (!ruleContent) {
+ ruleContent = {
+ conditions: getRuleCondition(type),
+ actions: [],
+ rule_id: rule,
+ default: true,
+ enabled: true,
+ };
+ ur.push(ruleContent);
+ }
+ ruleContent.actions = getTypeActions(type);
+
+ mx.setAccountData('m.push_rules', content);
+ };
+
+ const dmRule = underride.find((rule) => rule.rule_id === DM);
+ const encDmRule = underride.find((rule) => rule.rule_id === ENC_DM);
+ const roomRule = underride.find((rule) => rule.rule_id === ROOM);
+ const encRoomRule = underride.find((rule) => rule.rule_id === ENC_ROOM);
+
+ if (dmRule) rulesToType[DM] = getActionType(dmRule);
+ if (encDmRule) rulesToType[ENC_DM] = getActionType(encDmRule);
+ if (roomRule) rulesToType[ROOM] = getActionType(roomRule);
+ if (encRoomRule) rulesToType[ENC_ROOM] = getActionType(encRoomRule);
+
+ return [rulesToType, setRule];
+}
+
+function GlobalNotification() {
+ const [rulesToType, setRule] = useGlobalNotif();
+
+ const onSelect = (evt, rule) => {
+ openReusableContextMenu(
+ 'bottom',
+ getEventCords(evt, '.btn-surface'),
+ (requestClose) => (
+ <NotificationSelector
+ value={rulesToType[rule]}
+ onSelect={(value) => {
+ if (rulesToType[rule] !== value) setRule(rule, value);
+ requestClose();
+ }}
+ />
+ ),
+ );
+ };
+
+ return (
+ <div className="global-notification">
+ <MenuHeader>Global Notifications</MenuHeader>
+ <SettingTile
+ title="Direct messages"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, DM)} iconSrc={ChevronBottomIC}>
+ { typeToLabel[rulesToType[DM]] }
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all direct message.</Text>}
+ />
+ <SettingTile
+ title="Encrypted direct messages"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, ENC_DM)} iconSrc={ChevronBottomIC}>
+ {typeToLabel[rulesToType[ENC_DM]]}
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all encrypted direct message.</Text>}
+ />
+ <SettingTile
+ title="Rooms messages"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, ROOM)} iconSrc={ChevronBottomIC}>
+ {typeToLabel[rulesToType[ROOM]]}
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all room message.</Text>}
+ />
+ <SettingTile
+ title="Encrypted rooms messages"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, ENC_ROOM)} iconSrc={ChevronBottomIC}>
+ {typeToLabel[rulesToType[ENC_ROOM]]}
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all encrypted room message.</Text>}
+ />
+ </div>
+ );
+}
+
+export default GlobalNotification;
--- /dev/null
+import React from 'react';
+import './KeywordNotification.scss';
+
+import initMatrix from '../../../client/initMatrix';
+import { openReusableContextMenu } from '../../../client/action/navigation';
+import { getEventCords } from '../../../util/common';
+
+import Text from '../../atoms/text/Text';
+import Chip from '../../atoms/chip/Chip';
+import Input from '../../atoms/input/Input';
+import Button from '../../atoms/button/Button';
+import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
+import SettingTile from '../setting-tile/SettingTile';
+
+import NotificationSelector from './NotificationSelector';
+
+import ChevronBottomIC from '../../../../public/res/ic/outlined/chevron-bottom.svg';
+import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
+
+import { useAccountData } from '../../hooks/useAccountData';
+import {
+ notifType, typeToLabel, getActionType, getTypeActions,
+} from './GlobalNotification';
+
+const DISPLAY_NAME = '.m.rule.contains_display_name';
+const ROOM_PING = '.m.rule.roomnotif';
+const USERNAME = '.m.rule.contains_user_name';
+const KEYWORD = 'keyword';
+
+function useKeywordNotif() {
+ const mx = initMatrix.matrixClient;
+ const pushRules = useAccountData('m.push_rules');
+ const override = pushRules?.global?.override ?? [];
+ const content = pushRules?.global?.content ?? [];
+
+ const rulesToType = {
+ [DISPLAY_NAME]: notifType.NOISY,
+ [ROOM_PING]: notifType.NOISY,
+ [USERNAME]: notifType.NOISY,
+ };
+
+ const setRule = (rule, type) => {
+ const evtContent = pushRules ?? {};
+ if (!evtContent.global) evtContent.global = {};
+ if (!evtContent.global.override) evtContent.global.override = [];
+ if (!evtContent.global.content) evtContent.global.content = [];
+ const or = evtContent.global.override;
+ const ct = evtContent.global.content;
+
+ if (rule === DISPLAY_NAME || rule === ROOM_PING) {
+ let orRule = or.find((r) => r?.rule_id === rule);
+ if (!orRule) {
+ orRule = {
+ conditions: [],
+ actions: [],
+ rule_id: rule,
+ default: true,
+ enabled: true,
+ };
+ or.push(orRule);
+ }
+ if (rule === DISPLAY_NAME) {
+ orRule.conditions = [{ kind: 'contains_display_name' }];
+ orRule.actions = getTypeActions(type, true);
+ } else {
+ orRule.conditions = [
+ { kind: 'event_match', key: 'content.body', pattern: '@room' },
+ { kind: 'sender_notification_permission', key: 'room' },
+ ];
+ orRule.actions = getTypeActions(type, true);
+ }
+ } else if (rule === USERNAME) {
+ let usernameRule = ct.find((r) => r?.rule_id === rule);
+ if (!usernameRule) {
+ const userId = mx.getUserId();
+ const username = userId.match(/^@?(\S+):(\S+)$/)?.[1] ?? userId;
+ usernameRule = {
+ actions: [],
+ default: true,
+ enabled: true,
+ pattern: username,
+ rule_id: rule,
+ };
+ ct.push(usernameRule);
+ }
+ usernameRule.actions = getTypeActions(type, true);
+ } else {
+ const keyRules = ct.filter((r) => r.rule_id !== USERNAME);
+ keyRules.forEach((r) => {
+ // eslint-disable-next-line no-param-reassign
+ r.actions = getTypeActions(type, true);
+ });
+ }
+
+ mx.setAccountData('m.push_rules', evtContent);
+ };
+
+ const addKeyword = (keyword) => {
+ if (content.find((r) => r.rule_id === keyword)) return;
+ content.push({
+ rule_id: keyword,
+ pattern: keyword,
+ enabled: true,
+ default: false,
+ actions: getTypeActions(rulesToType[KEYWORD] ?? notifType.NOISY, true),
+ });
+ mx.setAccountData('m.push_rules', pushRules);
+ };
+ const removeKeyword = (rule) => {
+ pushRules.global.content = content.filter((r) => r.rule_id !== rule.rule_id);
+ mx.setAccountData('m.push_rules', pushRules);
+ };
+
+ const dsRule = override.find((rule) => rule.rule_id === DISPLAY_NAME);
+ const roomRule = override.find((rule) => rule.rule_id === ROOM_PING);
+ const usernameRule = content.find((rule) => rule.rule_id === USERNAME);
+ const keywordRule = content.find((rule) => rule.rule_id !== USERNAME);
+
+ if (dsRule) rulesToType[DISPLAY_NAME] = getActionType(dsRule);
+ if (roomRule) rulesToType[ROOM_PING] = getActionType(roomRule);
+ if (usernameRule) rulesToType[USERNAME] = getActionType(usernameRule);
+ if (keywordRule) rulesToType[KEYWORD] = getActionType(keywordRule);
+
+ return {
+ rulesToType,
+ pushRules,
+ setRule,
+ addKeyword,
+ removeKeyword,
+ };
+}
+
+function GlobalNotification() {
+ const {
+ rulesToType,
+ pushRules,
+ setRule,
+ addKeyword,
+ removeKeyword,
+ } = useKeywordNotif();
+
+ const keywordRules = pushRules?.global?.content.filter((r) => r.rule_id !== USERNAME) ?? [];
+
+ const onSelect = (evt, rule) => {
+ openReusableContextMenu(
+ 'bottom',
+ getEventCords(evt, '.btn-surface'),
+ (requestClose) => (
+ <NotificationSelector
+ value={rulesToType[rule]}
+ onSelect={(value) => {
+ if (rulesToType[rule] !== value) setRule(rule, value);
+ requestClose();
+ }}
+ />
+ ),
+ );
+ };
+
+ const handleSubmit = (evt) => {
+ evt.preventDefault();
+ const { keywordInput } = evt.target.elements;
+ const value = keywordInput.value.trim();
+ if (value === '') return;
+ addKeyword(value);
+ keywordInput.value = '';
+ };
+
+ return (
+ <div className="keyword-notification">
+ <MenuHeader>Mentions & keywords</MenuHeader>
+ <SettingTile
+ title="Message containing my display name"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, DISPLAY_NAME)} iconSrc={ChevronBottomIC}>
+ { typeToLabel[rulesToType[DISPLAY_NAME]] }
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all message containing your display name.</Text>}
+ />
+ <SettingTile
+ title="Message containing my username"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, USERNAME)} iconSrc={ChevronBottomIC}>
+ { typeToLabel[rulesToType[USERNAME]] }
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all message containing your username.</Text>}
+ />
+ <SettingTile
+ title="Message containing @room"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, ROOM_PING)} iconSrc={ChevronBottomIC}>
+ {typeToLabel[rulesToType[ROOM_PING]]}
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all messages containing @room.</Text>}
+ />
+ { rulesToType[KEYWORD] && (
+ <SettingTile
+ title="Message containing keywords"
+ options={(
+ <Button onClick={(evt) => onSelect(evt, KEYWORD)} iconSrc={ChevronBottomIC}>
+ {typeToLabel[rulesToType[KEYWORD]]}
+ </Button>
+ )}
+ content={<Text variant="b3">Default notification settings for all message containing keywords.</Text>}
+ />
+ )}
+ <SettingTile
+ title="Keywords"
+ content={(
+ <div className="keyword-notification__keyword">
+ <Text variant="b3">Get notification when a message contains keyword.</Text>
+ <form onSubmit={handleSubmit}>
+ <Input name="keywordInput" required />
+ <Button variant="primary" type="submit">Add</Button>
+ </form>
+ {keywordRules.length > 0 && (
+ <div>
+ {keywordRules.map((rule) => (
+ <Chip
+ iconSrc={CrossIC}
+ key={rule.rule_id}
+ text={rule.pattern}
+ iconColor={CrossIC}
+ onClick={() => removeKeyword(rule)}
+ />
+ ))}
+ </div>
+ )}
+ </div>
+ )}
+ />
+ </div>
+ );
+}
+
+export default GlobalNotification;
--- /dev/null
+.keyword-notification {
+ &__keyword {
+ & form,
+ & > div:last-child {
+ display: flex;
+ gap: var(--sp-tight);
+ }
+
+ & form {
+ margin: var(--sp-ultra-tight) 0 var(--sp-normal);
+ .input-container {
+ flex-grow: 1;
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { MenuHeader, MenuItem } from '../../atoms/context-menu/ContextMenu';
+
+import CheckIC from '../../../../public/res/ic/outlined/check.svg';
+
+function NotificationSelector({
+ value, onSelect,
+}) {
+ return (
+ <div>
+ <MenuHeader>Notification</MenuHeader>
+ <MenuItem iconSrc={value === 'off' ? CheckIC : null} variant={value === 'off' ? 'positive' : 'surface'} onClick={() => onSelect('off')}>Off</MenuItem>
+ <MenuItem iconSrc={value === 'on' ? CheckIC : null} variant={value === 'on' ? 'positive' : 'surface'} onClick={() => onSelect('on')}>On</MenuItem>
+ <MenuItem iconSrc={value === 'noisy' ? CheckIC : null} variant={value === 'noisy' ? 'positive' : 'surface'} onClick={() => onSelect('noisy')}>Noisy</MenuItem>
+ </div>
+ );
+}
+
+NotificationSelector.propTypes = {
+ value: PropTypes.oneOf(['off', 'on', 'noisy']).isRequired,
+ onSelect: PropTypes.func.isRequired,
+};
+
+export default NotificationSelector;
useEffect(() => {
requestAnimationFrame(() => {
- scrollRef.current.scrollTop = 0;
+ if (scrollRef.current) {
+ scrollRef.current.scrollTop = 0;
+ }
});
}, [selectedTab]);
}
};
- const notifs = initMatrix.notifications;
+ const noti = initMatrix.notifications;
const renderRoomSelector = (item) => {
let imageSrc = null;
let iconSrc = null;
iconSrc = joinRuleToIconSrc(item.room.getJoinRule(), item.type === 'space');
}
- const isUnread = notifs.hasNoti(item.roomId);
- const noti = notifs.getNoti(item.roomId);
-
return (
<RoomSelector
key={item.roomId}
roomId={item.roomId}
imageSrc={imageSrc}
iconSrc={iconSrc}
- isUnread={isUnread}
- notificationCount={noti.total}
- isAlert={noti.highlight > 0}
+ isUnread={noti.hasNoti(item.roomId)}
+ notificationCount={noti.getTotalNoti(item.roomId)}
+ isAlert={noti.getHighlightNoti(item.roomId) > 0}
onClick={() => openItem(item.roomId, item.type)}
/>
);
size="small"
>
<div className="search-dialog">
- <form className="search-dialog__input" onSubmit={(e) => { e.preventDefault(); openFirstResult()}}>
+ <form className="search-dialog__input" onSubmit={(e) => { e.preventDefault(); openFirstResult(); }}>
<RawIcon src={SearchIC} size="small" />
<Input
onChange={handleOnChange}
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack';
+import GlobalNotification from '../../molecules/global-notification/GlobalNotification';
+import KeywordNotification from '../../molecules/global-notification/KeywordNotification';
import ProfileEditor from '../profile-editor/ProfileEditor';
import CrossSigning from './CrossSigning';
};
return (
- <div className="settings-notifications">
- <MenuHeader>Notification & Sound</MenuHeader>
- <SettingTile
- title="Desktop notification"
- options={renderOptions()}
- content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
- />
- <SettingTile
- title="Notification Sound"
- options={(
- <Toggle
- isActive={settings.isNotificationSounds}
- onToggle={() => { toggleNotificationSounds(); updateState({}); }}
- />
- )}
- content={<Text variant="b3">Play sound when new messages arrive.</Text>}
- />
- </div>
+ <>
+ <div className="settings-notifications">
+ <MenuHeader>Notification & Sound</MenuHeader>
+ <SettingTile
+ title="Desktop notification"
+ options={renderOptions()}
+ content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
+ />
+ <SettingTile
+ title="Notification Sound"
+ options={(
+ <Toggle
+ isActive={settings.isNotificationSounds}
+ onToggle={() => { toggleNotificationSounds(); updateState({}); }}
+ />
+ )}
+ content={<Text variant="b3">Play sound when new messages arrive.</Text>}
+ />
+ </div>
+ <GlobalNotification />
+ <KeywordNotification />
+ </>
);
}
}
.settings-appearance__card,
.settings-notifications,
+.global-notification,
+.keyword-notification,
.settings-security__card,
.settings-security .device-manage,
.settings-about__card,
},
PREPARED: (prevState) => {
console.log('PREPARED state');
- console.log('previous state: ', prevState);
+ console.log('Previous state: ', prevState);
// TODO: remove global.initMatrix at end
global.initMatrix = this;
if (prevState === null) {
this.roomsInput = new RoomsInput(this.matrixClient, this.roomList);
this.notifications = new Notifications(this.roomList);
this.emit('init_loading_finished');
+ this.notifications._initNoti();
+ } else {
+ this.notifications._initNoti();
}
},
RECONNECTING: () => {
import cons from './cons';
import navigation from './navigation';
import settings from './settings';
+import { setFavicon } from '../../util/common';
+
+import LogoSVG from '../../../public/res/svg/cinny.svg';
+import LogoUnreadSVG from '../../../public/res/svg/cinny-unread.svg';
+import LogoHighlightSVG from '../../../public/res/svg/cinny-highlight.svg';
function isNotifEvent(mEvent) {
const eType = mEvent.getType();
this.roomIdToNoti = new Map();
- this._initNoti();
+ // this._initNoti();
this._listenEvents();
// Ask for permission by default after loading
window.Notification?.requestPermission();
-
- // TODO:
- window.notifications = this;
}
- _initNoti() {
+ async _initNoti() {
+ this.roomIdToNoti = new Map();
+
const addNoti = (roomId) => {
const room = this.matrixClient.getRoom(roomId);
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return;
};
[...this.roomList.rooms].forEach(addNoti);
[...this.roomList.directs].forEach(addNoti);
+ this._updateFavicon();
}
doesRoomHaveUnread(room) {
}
getTotalNoti(roomId) {
- const { total } = this.getNoti(roomId);
+ const { total, highlight } = this.getNoti(roomId);
+ if (highlight > total) return highlight;
return total;
}
}
}
+ async _updateFavicon() {
+ let unread = false;
+ let highlight = false;
+ [...this.roomIdToNoti.values()].find((noti) => {
+ if (!unread) {
+ unread = noti.total > 0 || noti.highlight > 0;
+ }
+ highlight = noti.highlight > 0;
+ if (unread && highlight) return true;
+ return false;
+ });
+ if (!unread) {
+ setFavicon(LogoSVG);
+ return;
+ }
+ setFavicon(highlight ? LogoHighlightSVG : LogoUnreadSVG);
+ }
+
_setNoti(roomId, total, highlight) {
const addNoti = (id, t, h, fromId) => {
const prevTotal = this.roomIdToNoti.get(id)?.total ?? null;
allParentSpaces.forEach((spaceId) => {
addNoti(spaceId, addT, addH, roomId);
});
+ this._updateFavicon();
}
_deleteNoti(roomId, total, highlight) {
allParentSpaces.forEach((spaceId) => {
removeNoti(spaceId, total, highlight, roomId);
});
+ this._updateFavicon();
}
async _displayPopupNoti(mEvent, room) {
return [...text][0];
}
+export function cssVar(name) {
+ return getComputedStyle(document.body).getPropertyValue(name);
+}
+
+export function setFavicon(url) {
+ const oldFav = document.querySelector('[rel=icon]');
+ oldFav.parentElement.removeChild(oldFav);
+ const fav = document.createElement('link');
+ fav.rel = 'icon';
+ fav.href = url;
+ document.head.appendChild(fav);
+}
+
export function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);