mirror of
				https://github.com/cinnyapp/cinny.git
				synced 2025-11-04 06:20:28 +03:00 
			
		
		
		
	Notification settings (#807)
* Add account data hook * Add options to change global notification * Add options to add keywords * Fix wrong notification count * Show unread badge in favicon (#251) * Fix favicon badge * Fix bug * Fix favicon not updating * Add favicon svg's * remove dead code
This commit is contained in:
		
							parent
							
								
									ac155bbf4c
								
							
						
					
					
						commit
						584fa87bbd
					
				
					 15 changed files with 587 additions and 39 deletions
				
			
		
							
								
								
									
										13
									
								
								public/res/svg/cinny-highlight.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								public/res/svg/cinny-highlight.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										13
									
								
								public/res/svg/cinny-unread.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								public/res/svg/cinny-unread.svg
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,13 @@
 | 
			
		|||
<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>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.5 KiB  | 
| 
						 | 
				
			
			@ -1,8 +1,4 @@
 | 
			
		|||
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({
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										22
									
								
								src/app/hooks/useAccountData.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/app/hooks/useAccountData.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										174
									
								
								src/app/molecules/global-notification/GlobalNotification.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										174
									
								
								src/app/molecules/global-notification/GlobalNotification.jsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,174 @@
 | 
			
		|||
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;
 | 
			
		||||
							
								
								
									
										239
									
								
								src/app/molecules/global-notification/KeywordNotification.jsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										239
									
								
								src/app/molecules/global-notification/KeywordNotification.jsx
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,239 @@
 | 
			
		|||
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;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,16 @@
 | 
			
		|||
.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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
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;
 | 
			
		||||
| 
						 | 
				
			
			@ -56,7 +56,9 @@ function Drawer() {
 | 
			
		|||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    requestAnimationFrame(() => {
 | 
			
		||||
      scrollRef.current.scrollTop = 0;
 | 
			
		||||
      if (scrollRef.current) {
 | 
			
		||||
        scrollRef.current.scrollTop = 0;
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }, [selectedTab]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -168,7 +168,7 @@ function Search() {
 | 
			
		|||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const notifs = initMatrix.notifications;
 | 
			
		||||
  const noti = initMatrix.notifications;
 | 
			
		||||
  const renderRoomSelector = (item) => {
 | 
			
		||||
    let imageSrc = null;
 | 
			
		||||
    let iconSrc = null;
 | 
			
		||||
| 
						 | 
				
			
			@ -178,9 +178,6 @@ function Search() {
 | 
			
		|||
      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}
 | 
			
		||||
| 
						 | 
				
			
			@ -189,9 +186,9 @@ function Search() {
 | 
			
		|||
        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)}
 | 
			
		||||
      />
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			@ -207,7 +204,7 @@ function Search() {
 | 
			
		|||
      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}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -25,6 +25,8 @@ import SettingTile from '../../molecules/setting-tile/SettingTile';
 | 
			
		|||
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';
 | 
			
		||||
| 
						 | 
				
			
			@ -150,24 +152,28 @@ function NotificationsSection() {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  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 />
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,8 @@
 | 
			
		|||
}
 | 
			
		||||
.settings-appearance__card,
 | 
			
		||||
.settings-notifications,
 | 
			
		||||
.global-notification,
 | 
			
		||||
.keyword-notification,
 | 
			
		||||
.settings-security__card,
 | 
			
		||||
.settings-security .device-manage,
 | 
			
		||||
.settings-about__card,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,7 +67,7 @@ class InitMatrix extends EventEmitter {
 | 
			
		|||
      },
 | 
			
		||||
      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) {
 | 
			
		||||
| 
						 | 
				
			
			@ -76,6 +76,9 @@ class InitMatrix extends EventEmitter {
 | 
			
		|||
          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: () => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,6 +5,11 @@ import { selectRoom } from '../action/navigation';
 | 
			
		|||
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();
 | 
			
		||||
| 
						 | 
				
			
			@ -37,17 +42,16 @@ class Notifications extends EventEmitter {
 | 
			
		|||
 | 
			
		||||
    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;
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +63,7 @@ class Notifications extends EventEmitter {
 | 
			
		|||
    };
 | 
			
		||||
    [...this.roomList.rooms].forEach(addNoti);
 | 
			
		||||
    [...this.roomList.directs].forEach(addNoti);
 | 
			
		||||
    this._updateFavicon();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  doesRoomHaveUnread(room) {
 | 
			
		||||
| 
						 | 
				
			
			@ -104,7 +109,8 @@ class Notifications extends EventEmitter {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  getTotalNoti(roomId) {
 | 
			
		||||
    const { total } = this.getNoti(roomId);
 | 
			
		||||
    const { total, highlight } = this.getNoti(roomId);
 | 
			
		||||
    if (highlight > total) return highlight;
 | 
			
		||||
    return total;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -129,6 +135,24 @@ class Notifications extends EventEmitter {
 | 
			
		|||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
| 
						 | 
				
			
			@ -155,6 +179,7 @@ class Notifications extends EventEmitter {
 | 
			
		|||
    allParentSpaces.forEach((spaceId) => {
 | 
			
		||||
      addNoti(spaceId, addT, addH, roomId);
 | 
			
		||||
    });
 | 
			
		||||
    this._updateFavicon();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _deleteNoti(roomId, total, highlight) {
 | 
			
		||||
| 
						 | 
				
			
			@ -187,6 +212,7 @@ class Notifications extends EventEmitter {
 | 
			
		|||
    allParentSpaces.forEach((spaceId) => {
 | 
			
		||||
      removeNoti(spaceId, total, highlight, roomId);
 | 
			
		||||
    });
 | 
			
		||||
    this._updateFavicon();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async _displayPopupNoti(mEvent, room) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -115,6 +115,19 @@ export function avatarInitials(text) {
 | 
			
		|||
  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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue