Compare commits

...
Author SHA1 Message Date
Ajay Bura 4385b8b1a3 Add favicon ico 2022-09-04 18:12:03 +05:30
Ajay Bura c2b33d4b6b Copy android/* icons to dist 2022-09-04 18:01:05 +05:30
Ajay Bura d002f6de83 Remove favicons-webpack-plugin 2022-09-04 16:08:02 +05:30
Ajay Bura bdc10fb729 Fix multiple favicon changing request 2022-09-04 13:33:41 +05:30
Ajay Bura 584fa87bbd 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
2022-09-04 13:10:07 +05:30
Ajay Bura ac155bbf4c Fix commands (#791)
* Fix commands and added more

* Add /me & /shrug commands

* Add help command

* Fix cmd descriptions

* Add reason in command
2022-09-03 21:46:40 +05:30
Krishan 0f6f65045d Add npmrc so that newer node/npm version works (#806)
* Add .npmrc so that it works with newer npm

* Remove engine upper limit as it works with npmrc

* Lockfile maintainace, created new mapping with npm install

* Add npmrc so Docker doesnt fail on new npm version

* Revert 8a1946d558 will set renovate
2022-09-03 21:36:47 +05:30
50 changed files with 1055 additions and 3533 deletions

No files matched your search

+1
View File
@@ -0,0 +1 @@
legacy-peer-deps=true
+1 -1
View File
@@ -3,7 +3,7 @@ FROM node:17.9.0-alpine3.15 as builder
WORKDIR /src
COPY package.json package-lock.json /src/
COPY .npmrc package.json package-lock.json /src/
RUN npm ci
COPY . /src/
RUN npm run build
+23 -3400
View File
File diff suppressed because it is too large. Load diff
+2 -4
View File
@@ -4,8 +4,8 @@
"description": "Yet another matrix client",
"main": "index.js",
"engines": {
"npm": ">=6.14.8 <=8.5.5",
"node": ">=14.15.0 <=17.9.0"
"npm": ">=6.14.8",
"node": ">=14.15.0"
},
"scripts": {
"start": "webpack serve --config ./webpack.dev.js --open",
@@ -65,8 +65,6 @@
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.1",
"eslint-plugin-react-hooks": "^4.6.0",
"favicons": "^6.2.2",
"favicons-webpack-plugin": "^5.0.2",
"html-loader": "^4.1.0",
"html-webpack-plugin": "^5.3.1",
"mini-css-extract-plugin": "^2.6.1",
Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+22
View File
@@ -15,6 +15,28 @@
<meta property="og:image" content="https://cinny.in/assets/favicon-48x48.png">
<meta property="og:description" content="A Matrix client where you can enjoy the conversation using simple, elegant and secure interface protected by e2ee with the power of open source.">
<meta name="theme-color" content="#000000">
<link id="favicon" rel="shortcut icon" href="./favicon.ico" />
<link rel="manifest" href="./manifest.json" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="application-name" content="Cinny" />
<meta name="apple-mobile-web-app-title" content="Cinny" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="apple-touch-icon" sizes="57x57" href="./res/apple/apple-touch-icon-57x57.png"/>
<link rel="apple-touch-icon" sizes="60x60" href="./res/apple/apple-touch-icon-60x60.png"/>
<link rel="apple-touch-icon" sizes="72x72" href="./res/apple/apple-touch-icon-72x72.png"/>
<link rel="apple-touch-icon" sizes="76x76" href="./res/apple/apple-touch-icon-76x76.png"/>
<link rel="apple-touch-icon" sizes="114x114" href="./res/apple/apple-touch-icon-114x114.png"/>
<link rel="apple-touch-icon" sizes="120x120" href="./res/apple/apple-touch-icon-120x120.png"/>
<link rel="apple-touch-icon" sizes="144x144" href="./res/apple/apple-touch-icon-144x144.png"/>
<link rel="apple-touch-icon" sizes="152x152" href="./res/apple/apple-touch-icon-152x152.png"/>
<link rel="apple-touch-icon" sizes="167x167" href="./res/apple/apple-touch-icon-167x167.png"/>
<link rel="apple-touch-icon" sizes="180x180" href="./res/apple/apple-touch-icon-180x180.png"/>
</head>
<body id="appBody">
<div id="root"></div>
+59
View File
@@ -0,0 +1,59 @@
{
"name": "Cinny",
"short_name": "Cinny",
"description": "Yet another matrix client",
"dir": "auto",
"lang": "en-US",
"display": "standalone",
"orientation": "portrait",
"start_url": "/",
"background_color": "#fff",
"theme_color": "#fff",
"icons": [
{
"src": "android-chrome-36x36.png",
"sizes": "36x36",
"type": "image/png"
},
{
"src": "android-chrome-48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "android-chrome-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "android-chrome-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "android-chrome-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "android-chrome-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

+13
View 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
View 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 -5
View File
@@ -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
View 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;
}
@@ -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;
@@ -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;
+3 -1
View File
@@ -56,7 +56,9 @@ function Drawer() {
useEffect(() => {
requestAnimationFrame(() => {
scrollRef.current.scrollTop = 0;
if (scrollRef.current) {
scrollRef.current.scrollTop = 0;
}
});
}, [selectedTab]);
@@ -11,7 +11,7 @@ import { selectRoom, openReusableContextMenu } from '../../../client/action/navi
import * as roomActions from '../../../client/action/room';
import {
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices
getUsername, getUsernameOfRoomMember, getPowerLabel, hasDMWith, hasDevices,
} from '../../../util/matrixUtil';
import { getEventCords } from '../../../util/common';
import colorMXID from '../../../util/colorMXID';
@@ -209,19 +209,18 @@ function ProfileFooter({ roomId, userId, onRequestClose }) {
};
const toggleIgnore = async () => {
const ignoredUsers = mx.getIgnoredUsers();
const uIndex = ignoredUsers.indexOf(userId);
if (uIndex >= 0) {
if (uIndex === -1) return;
ignoredUsers.splice(uIndex, 1);
} else ignoredUsers.push(userId);
const isIgnored = mx.getIgnoredUsers().includes(userId);
try {
setIsIgnoring(true);
await mx.setIgnoredUsers(ignoredUsers);
if (isIgnored) {
await roomActions.unignore([userId]);
} else {
await roomActions.ignore([userId]);
}
if (isMountedRef.current === false) return;
setIsUserIgnored(uIndex < 0);
setIsUserIgnored(!isIgnored);
setIsIgnoring(false);
} catch {
setIsIgnoring(false);
+9 -45
View File
@@ -8,13 +8,6 @@ import twemoji from 'twemoji';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix';
import { toggleMarkdown } from '../../../client/action/settings';
import * as roomActions from '../../../client/action/room';
import {
openCreateRoom,
openPublicRooms,
openInviteUser,
} from '../../../client/action/navigation';
import { getEmojiForCompletion } from '../emoji-board/custom-emoji';
import AsyncSearch from '../../../util/AsyncSearch';
@@ -22,37 +15,7 @@ import Text from '../../atoms/text/Text';
import ScrollView from '../../atoms/scroll/ScrollView';
import FollowingMembers from '../../molecules/following-members/FollowingMembers';
import { addRecentEmoji, getRecentEmojis } from '../emoji-board/recent';
const commands = [{
name: 'markdown',
description: 'Toggle markdown for messages.',
exe: () => toggleMarkdown(),
}, {
name: 'startDM',
isOptions: true,
description: 'Start direct message with user. Example: /startDM/@johndoe.matrix.org',
exe: (roomId, searchTerm) => openInviteUser(undefined, searchTerm),
}, {
name: 'createRoom',
description: 'Create new room',
exe: () => openCreateRoom(),
}, {
name: 'join',
isOptions: true,
description: 'Join room with alias. Example: /join/#cinny:matrix.org',
exe: (roomId, searchTerm) => openPublicRooms(searchTerm),
}, {
name: 'leave',
description: 'Leave current room',
exe: (roomId) => {
roomActions.leave(roomId);
},
}, {
name: 'invite',
isOptions: true,
description: 'Invite user to room. Example: /invite/@johndoe:matrix.org',
exe: (roomId, searchTerm) => openInviteUser(roomId, searchTerm),
}];
import commands from './commands';
function CmdItem({ onClick, children }) {
return (
@@ -71,16 +34,16 @@ function renderSuggestions({ prefix, option, suggestions }, fireCmd) {
const cmdOptString = (typeof option === 'string') ? `/${option}` : '/?';
return cmds.map((cmd) => (
<CmdItem
key={cmd.name}
key={cmd}
onClick={() => {
fireCmd({
prefix: cmdPrefix,
option,
result: cmd,
result: commands[cmd],
});
}}
>
<Text variant="b2">{`${cmd.name}${cmd.isOptions ? cmdOptString : ''}`}</Text>
<Text variant="b2">{`${cmd}${cmd.isOptions ? cmdOptString : ''}`}</Text>
</CmdItem>
));
}
@@ -209,8 +172,8 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
const mx = initMatrix.matrixClient;
const setupSearch = {
'/': () => {
asyncSearch.setup(commands, { keys: ['name'], isContain: true });
setCmd({ prefix, suggestions: commands });
asyncSearch.setup(Object.keys(commands), { isContain: true });
setCmd({ prefix, suggestions: Object.keys(commands) });
},
':': () => {
const parentIds = initMatrix.roomList.getAllParentSpaces(roomId);
@@ -242,8 +205,9 @@ function RoomViewCmdBar({ roomId, roomTimeline, viewEvent }) {
}
function fireCmd(myCmd) {
if (myCmd.prefix === '/') {
myCmd.result.exe(roomId, myCmd.option);
viewEvent.emit('cmd_fired');
viewEvent.emit('cmd_fired', {
replace: `/${myCmd.result.name}`,
});
}
if (myCmd.prefix === ':') {
if (!myCmd.result.mxc) addRecentEmoji(myCmd.result.unicode);
+34 -7
View File
@@ -21,6 +21,7 @@ import ScrollView from '../../atoms/scroll/ScrollView';
import { MessageReply } from '../../molecules/message/Message';
import StickerBoard from '../sticker-board/StickerBoard';
import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog';
import CirclePlusIC from '../../../../public/res/ic/outlined/circle-plus.svg';
import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg';
@@ -33,6 +34,8 @@ import MarkdownIC from '../../../../public/res/ic/outlined/markdown.svg';
import FileIC from '../../../../public/res/ic/outlined/file.svg';
import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
import commands from './commands';
const CMD_REGEX = /(^\/|:|@)(\S*)$/;
let isTyping = false;
let isCmdActivated = false;
@@ -182,30 +185,54 @@ function RoomViewInput({
};
}, [roomId]);
const sendMessage = async () => {
requestAnimationFrame(() => deactivateCmdAndEmit());
const msgBody = textAreaRef.current.value;
const sendBody = async (body, msgType = 'm.text') => {
if (roomsInput.isSending(roomId)) return;
if (msgBody.trim() === '' && attachment === null) return;
sendIsTyping(false);
roomsInput.setMessage(roomId, msgBody);
roomsInput.setMessage(roomId, body);
if (attachment !== null) {
roomsInput.setAttachment(roomId, attachment);
}
textAreaRef.current.disabled = true;
textAreaRef.current.style.cursor = 'not-allowed';
await roomsInput.sendInput(roomId);
await roomsInput.sendInput(roomId, msgType);
textAreaRef.current.disabled = false;
textAreaRef.current.style.cursor = 'unset';
focusInput();
textAreaRef.current.value = roomsInput.getMessage(roomId);
viewEvent.emit('message_sent');
textAreaRef.current.style.height = 'unset';
if (replyTo !== null) setReplyTo(null);
};
const processCommand = (cmdBody) => {
const spaceIndex = cmdBody.indexOf(' ');
const cmdName = cmdBody.slice(1, spaceIndex > -1 ? spaceIndex : undefined);
const cmdData = spaceIndex > -1 ? cmdBody.slice(spaceIndex + 1) : '';
if (!commands[cmdName]) {
confirmDialog('Invalid Command', `"${cmdName}" is not a valid command.`, 'Alright');
return;
}
if (['me', 'shrug'].includes(cmdName)) {
commands[cmdName].exe(roomId, cmdData, (message, msgType) => sendBody(message, msgType));
return;
}
commands[cmdName].exe(roomId, cmdData);
};
const sendMessage = async () => {
requestAnimationFrame(() => deactivateCmdAndEmit());
const msgBody = textAreaRef.current.value.trim();
if (msgBody.startsWith('/')) {
processCommand(msgBody.trim());
textAreaRef.current.value = '';
textAreaRef.current.style.height = 'unset';
return;
}
if (msgBody === '' && attachment === null) return;
sendBody(msgBody, 'm.text');
};
const handleSendSticker = async (data) => {
roomsInput.sendSticker(roomId, data);
};
+211
View File
@@ -0,0 +1,211 @@
import React from 'react';
import './commands.scss';
import initMatrix from '../../../client/initMatrix';
import * as roomActions from '../../../client/action/room';
import { hasDMWith, hasDevices } from '../../../util/matrixUtil';
import { selectRoom, openReusableDialog } from '../../../client/action/navigation';
import Text from '../../atoms/text/Text';
import SettingTile from '../../molecules/setting-tile/SettingTile';
const MXID_REG = /^@\S+:\S+$/;
const ROOM_ID_ALIAS_REG = /^(#|!)\S+:\S+$/;
const ROOM_ID_REG = /^!\S+:\S+$/;
const MXC_REG = /^mxc:\/\/\S+$/;
export function processMxidAndReason(data) {
let reason;
let idData = data;
const reasonMatch = data.match(/\s-r\s/);
if (reasonMatch) {
idData = data.slice(0, reasonMatch.index);
reason = data.slice(reasonMatch.index + reasonMatch[0].length);
if (reason.trim() === '') reason = undefined;
}
const rawIds = idData.split(' ');
const userIds = rawIds.filter((id) => id.match(MXID_REG));
return {
userIds,
reason,
};
}
const commands = {
me: {
name: 'me',
description: 'Display action',
exe: (roomId, data, onSuccess) => {
const body = data.trim();
if (body === '') return;
onSuccess(body, 'm.emote');
},
},
shrug: {
name: 'shrug',
description: 'Send ¯\\_(ツ)_/¯ as message',
exe: (roomId, data, onSuccess) => onSuccess(
`¯\\_(ツ)_/¯${data.trim() !== '' ? ` ${data}` : ''}`,
'm.text',
),
},
help: {
name: 'help',
description: 'View all commands',
// eslint-disable-next-line no-use-before-define
exe: () => openHelpDialog(),
},
startdm: {
name: 'startdm',
description: 'Start DM with user. Example: /startdm userId1 userId2',
exe: async (roomId, data) => {
const mx = initMatrix.matrixClient;
const rawIds = data.split(' ');
const userIds = rawIds.filter((id) => id.match(MXID_REG) && id !== mx.getUserId());
if (userIds.length === 0) return;
if (userIds.length === 1) {
const dmRoomId = hasDMWith(userIds[0]);
if (dmRoomId) {
selectRoom(dmRoomId);
return;
}
}
const devices = await Promise.all(userIds.map(hasDevices));
const isEncrypt = devices.every((hasDevice) => hasDevice);
const result = await roomActions.createDM(userIds, isEncrypt);
selectRoom(result.room_id);
},
},
join: {
name: 'join',
description: 'Join room with alias. Example: /join alias1 alias2',
exe: (roomId, data) => {
const rawIds = data.split(' ');
const roomIds = rawIds.filter((id) => id.match(ROOM_ID_ALIAS_REG));
roomIds.map((id) => roomActions.join(id));
},
},
leave: {
name: 'leave',
description: 'Leave current room.',
exe: (roomId, data) => {
if (data.trim() === '') {
roomActions.leave(roomId);
return;
}
const rawIds = data.split(' ');
const roomIds = rawIds.filter((id) => id.match(ROOM_ID_REG));
roomIds.map((id) => roomActions.leave(id));
},
},
invite: {
name: 'invite',
description: 'Invite user to room. Example: /invite userId1 userId2 [-r reason]',
exe: (roomId, data) => {
const { userIds, reason } = processMxidAndReason(data);
userIds.map((id) => roomActions.invite(roomId, id, reason));
},
},
disinvite: {
name: 'disinvite',
description: 'Disinvite user to room. Example: /disinvite userId1 userId2 [-r reason]',
exe: (roomId, data) => {
const { userIds, reason } = processMxidAndReason(data);
userIds.map((id) => roomActions.kick(roomId, id, reason));
},
},
kick: {
name: 'kick',
description: 'Kick user from room. Example: /kick userId1 userId2 [-r reason]',
exe: (roomId, data) => {
const { userIds, reason } = processMxidAndReason(data);
userIds.map((id) => roomActions.kick(roomId, id, reason));
},
},
ban: {
name: 'ban',
description: 'Ban user from room. Example: /ban userId1 userId2 [-r reason]',
exe: (roomId, data) => {
const { userIds, reason } = processMxidAndReason(data);
userIds.map((id) => roomActions.ban(roomId, id, reason));
},
},
unban: {
name: 'unban',
description: 'Unban user from room. Example: /unban userId1 userId2',
exe: (roomId, data) => {
const rawIds = data.split(' ');
const userIds = rawIds.filter((id) => id.match(MXID_REG));
userIds.map((id) => roomActions.unban(roomId, id));
},
},
ignore: {
name: 'ignore',
description: 'Ignore user. Example: /ignore userId1 userId2',
exe: (roomId, data) => {
const rawIds = data.split(' ');
const userIds = rawIds.filter((id) => id.match(MXID_REG));
if (userIds.length > 0) roomActions.ignore(userIds);
},
},
unignore: {
name: 'unignore',
description: 'Unignore user. Example: /unignore userId1 userId2',
exe: (roomId, data) => {
const rawIds = data.split(' ');
const userIds = rawIds.filter((id) => id.match(MXID_REG));
if (userIds.length > 0) roomActions.unignore(userIds);
},
},
myroomnick: {
name: 'myroomnick',
description: 'Change my room nick',
exe: (roomId, data) => {
const nick = data.trim();
if (nick === '') return;
roomActions.setMyRoomNick(roomId, nick);
},
},
myroomavatar: {
name: 'myroomavatar',
description: 'Change my room avatar. Example /myroomavatar mxc://xyzabc',
exe: (roomId, data) => {
if (data.match(MXC_REG)) {
roomActions.setMyRoomAvatar(roomId, data);
}
},
},
converttodm: {
name: 'converttodm',
description: 'Convert room to direct message',
exe: (roomId) => {
roomActions.convertToDm(roomId);
},
},
converttoroom: {
name: 'converttoroom',
description: 'Convert direct message to room',
exe: (roomId) => {
roomActions.convertToRoom(roomId);
},
},
};
function openHelpDialog() {
openReusableDialog(
<Text variant="s1" weight="medium">Commands</Text>,
() => (
<div className="commands-dialog">
{Object.keys(commands).map((cmdName) => (
<SettingTile
key={cmdName}
title={cmdName}
content={<Text variant="b3">{commands[cmdName].description}</Text>}
/>
))}
</div>
),
);
}
export default commands;
+10
View File
@@ -0,0 +1,10 @@
.commands-dialog {
& > * {
padding: var(--sp-tight) var(--sp-normal);
border-bottom: 1px solid var(--bg-surface-border);
&:last-child {
border-bottom: none;
margin-bottom: var(--sp-extra-loose);
}
}
}
+5 -8
View File
@@ -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}
+24 -18
View File
@@ -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 />
</>
);
}
+2
View File
@@ -38,6 +38,8 @@
}
.settings-appearance__card,
.settings-notifications,
.global-notification,
.keyword-notification,
.settings-security__card,
.settings-security .device-manage,
.settings-about__card,
+59 -6
View File
@@ -6,7 +6,7 @@ import { getIdServer } from '../../util/matrixUtil';
/**
* https://github.com/matrix-org/matrix-react-sdk/blob/1e6c6e9d800890c732d60429449bc280de01a647/src/Rooms.js#L73
* @param {string} roomId Id of room to add
* @param {string} userId User id to which dm
* @param {string} userId User id to which dm || undefined to remove
* @returns {Promise} A promise
*/
function addRoomToMDirect(roomId, userId) {
@@ -79,13 +79,23 @@ function guessDMRoomTargetId(room, myUserId) {
return oldestMember.userId;
}
function convertToDm(roomId) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
return addRoomToMDirect(roomId, guessDMRoomTargetId(room, mx.getUserId()));
}
function convertToRoom(roomId) {
return addRoomToMDirect(roomId, undefined);
}
/**
*
* @param {string} roomId
* @param {boolean} isDM
* @param {string[]} via
*/
async function join(roomIdOrAlias, isDM, via) {
async function join(roomIdOrAlias, isDM = false, via = undefined) {
const mx = initMatrix.matrixClient;
const roomIdParts = roomIdOrAlias.split(':');
const viaServers = via || [roomIdParts[1]];
@@ -150,10 +160,10 @@ async function create(options, isDM = false) {
}
}
async function createDM(userId, isEncrypted = true) {
async function createDM(userIdOrIds, isEncrypted = true) {
const options = {
is_direct: true,
invite: [userId],
invite: Array.isArray(userIdOrIds) ? userIdOrIds : [userIdOrIds],
visibility: 'private',
preset: 'trusted_private_chat',
initial_state: [],
@@ -262,10 +272,10 @@ async function createRoom(opts) {
return result;
}
async function invite(roomId, userId) {
async function invite(roomId, userId, reason) {
const mx = initMatrix.matrixClient;
const result = await mx.invite(roomId, userId);
const result = await mx.invite(roomId, userId, undefined, reason);
return result;
}
@@ -290,6 +300,21 @@ async function unban(roomId, userId) {
return result;
}
async function ignore(userIds) {
const mx = initMatrix.matrixClient;
let ignoredUsers = mx.getIgnoredUsers().concat(userIds);
ignoredUsers = [...new Set(ignoredUsers)];
await mx.setIgnoredUsers(ignoredUsers);
}
async function unignore(userIds) {
const mx = initMatrix.matrixClient;
const ignoredUsers = mx.getIgnoredUsers();
await mx.setIgnoredUsers(ignoredUsers.filter((id) => !userIds.includes(id)));
}
async function setPowerLevel(roomId, userId, powerLevel) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
@@ -300,9 +325,37 @@ async function setPowerLevel(roomId, userId, powerLevel) {
return result;
}
async function setMyRoomNick(roomId, nick) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const mEvent = room.currentState.getStateEvents('m.room.member', mx.getUserId());
const content = mEvent?.getContent();
if (!content) return;
await mx.sendStateEvent(roomId, 'm.room.member', {
...content,
displayname: nick,
}, mx.getUserId());
}
async function setMyRoomAvatar(roomId, mxc) {
const mx = initMatrix.matrixClient;
const room = mx.getRoom(roomId);
const mEvent = room.currentState.getStateEvents('m.room.member', mx.getUserId());
const content = mEvent?.getContent();
if (!content) return;
await mx.sendStateEvent(roomId, 'm.room.member', {
...content,
avatar_url: mxc,
}, mx.getUserId());
}
export {
convertToDm,
convertToRoom,
join, leave,
createDM, createRoom,
invite, kick, ban, unban,
ignore, unignore,
setPowerLevel,
setMyRoomNick, setMyRoomAvatar,
};
+4 -1
View File
@@ -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: () => {
+43 -6
View File
@@ -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();
@@ -32,22 +37,24 @@ class Notifications extends EventEmitter {
constructor(roomList) {
super();
this.initialized = false;
this.favicon = LogoSVG;
this.matrixClient = roomList.matrixClient;
this.roomList = roomList;
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.initialized = false;
this.roomIdToNoti = new Map();
const addNoti = (roomId) => {
const room = this.matrixClient.getRoom(roomId);
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return;
@@ -59,6 +66,9 @@ class Notifications extends EventEmitter {
};
[...this.roomList.rooms].forEach(addNoti);
[...this.roomList.directs].forEach(addNoti);
this.initialized = true;
this._updateFavicon();
}
doesRoomHaveUnread(room) {
@@ -104,7 +114,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 +140,30 @@ class Notifications extends EventEmitter {
}
}
async _updateFavicon() {
if (!this.initialized) return;
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;
});
let newFavicon = LogoSVG;
if (unread && !highlight) {
newFavicon = LogoUnreadSVG;
}
if (unread && highlight) {
newFavicon = LogoHighlightSVG;
}
if (newFavicon === this.favicon) return;
this.favicon = newFavicon;
setFavicon(this.favicon);
}
_setNoti(roomId, total, highlight) {
const addNoti = (id, t, h, fromId) => {
const prevTotal = this.roomIdToNoti.get(id)?.total ?? null;
@@ -155,6 +190,7 @@ class Notifications extends EventEmitter {
allParentSpaces.forEach((spaceId) => {
addNoti(spaceId, addT, addH, roomId);
});
this._updateFavicon();
}
_deleteNoti(roomId, total, highlight) {
@@ -187,6 +223,7 @@ class Notifications extends EventEmitter {
allParentSpaces.forEach((spaceId) => {
removeNoti(spaceId, total, highlight, roomId);
});
this._updateFavicon();
}
async _displayPopupNoti(mEvent, room) {
+14 -1
View File
@@ -257,10 +257,10 @@ class RoomList extends EventEmitter {
const latestMDirects = this.getMDirects();
latestMDirects.forEach((directId) => {
const myRoom = this.matrixClient.getRoom(directId);
if (this.mDirects.has(directId)) return;
this.mDirects.add(directId);
const myRoom = this.matrixClient.getRoom(directId);
if (myRoom === null) return;
if (myRoom.getMyMembership() === 'join') {
this.directs.add(directId);
@@ -268,6 +268,19 @@ class RoomList extends EventEmitter {
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
});
[...this.directs].forEach((directId) => {
if (latestMDirects.has(directId)) return;
this.mDirects.delete(directId);
const myRoom = this.matrixClient.getRoom(directId);
if (myRoom === null) return;
if (myRoom.getMyMembership() === 'join') {
this.directs.delete(directId);
this.rooms.add(directId);
this.emit(cons.events.roomList.ROOMLIST_UPDATED);
}
});
});
this.matrixClient.on('Room.name', (room) => {
+6 -4
View File
@@ -274,7 +274,7 @@ class RoomsInput extends EventEmitter {
return this.roomIdToInput.get(roomId)?.isSending || false;
}
async sendInput(roomId) {
async sendInput(roomId, msgType) {
const room = this.matrixClient.getRoom(roomId);
const input = this.getInput(roomId);
input.isSending = true;
@@ -288,7 +288,7 @@ class RoomsInput extends EventEmitter {
const rawMessage = input.message;
let content = {
body: rawMessage,
msgtype: 'm.text',
msgtype: msgType ?? 'm.text',
};
// Apply formatting if relevant
@@ -459,12 +459,14 @@ class RoomsInput extends EventEmitter {
const room = this.matrixClient.getRoom(roomId);
const isReply = typeof mEvent.getWireContent()['m.relates_to']?.['m.in_reply_to'] !== 'undefined';
const msgtype = mEvent.getWireContent().msgtype ?? 'm.text';
const content = {
body: ` * ${editedBody}`,
msgtype: 'm.text',
msgtype,
'm.new_content': {
body: editedBody,
msgtype: 'm.text',
msgtype,
},
'm.relates_to': {
event_id: mEvent.getId(),
+10
View File
@@ -115,6 +115,16 @@ export function avatarInitials(text) {
return [...text][0];
}
export function cssVar(name) {
return getComputedStyle(document.body).getPropertyValue(name);
}
export function setFavicon(url) {
const favicon = document.querySelector('#favicon');
if (!favicon) return;
favicon.setAttribute('href', url);
}
export function copyToClipboard(text) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
+1 -17
View File
@@ -1,5 +1,4 @@
const HtmlWebpackPlugin = require('html-webpack-plugin');
const FaviconsWebpackPlugin = require('favicons-webpack-plugin');
const CopyPlugin = require("copy-webpack-plugin");
const webpack = require('webpack');
@@ -52,27 +51,12 @@ module.exports = {
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new FaviconsWebpackPlugin({
logo: './public/res/svg/cinny.svg',
mode: 'webapp',
devMode: 'light',
favicons: {
appName: 'Cinny',
appDescription: 'Yet another matrix client',
developerName: 'Ajay Bura',
developerURL: 'https://github.com/ajbura',
icons: {
coast: false,
yandex: false,
appleStartup: false,
}
}
}),
new CopyPlugin({
patterns: [
{ from: 'olm.wasm' },
{ from: '_redirects' },
{ from: 'config.json' },
{ from: 'public/res/android'}
],
}),
new webpack.ProvidePlugin({