Fix muted room show unread indicator (#386)

* Move getNotifType function

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix bug in getNotiType

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Add isMuted prop in room selector

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix muted room show unread indicator

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix muted room notification visible in space

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Fix space shows muted room notification on load

Signed-off-by: Ajay Bura <ajbura@gmail.com>

* Toggle room mute when changed from other client

Signed-off-by: Ajay Bura <ajbura@gmail.com>
This commit is contained in:
Ajay Bura 2022-03-15 17:21:36 +05:30 committed by GitHub
parent 92a3a8d6fa
commit 70ffd7ded8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 106 additions and 30 deletions

View file

@ -32,28 +32,9 @@ const items = [{
type: cons.notifs.MUTE, type: cons.notifs.MUTE,
}]; }];
function getNotifType(roomId) {
const mx = initMatrix.matrixClient;
const pushRule = mx.getRoomPushRule('global', roomId);
if (typeof pushRule === 'undefined') {
const overridePushRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
if (typeof overridePushRules === 'undefined') return 0;
const isMuteOverride = overridePushRules.find((rule) => (
rule.rule_id === roomId
&& rule.actions[0] === 'dont_notify'
&& rule.conditions[0].kind === 'event_match'
));
return isMuteOverride ? cons.notifs.MUTE : cons.notifs.DEFAULT;
}
if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
return cons.notifs.MENTIONS_AND_KEYWORDS;
}
function setRoomNotifType(roomId, newType) { function setRoomNotifType(roomId, newType) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const { notifications } = initMatrix;
const roomPushRule = mx.getRoomPushRule('global', roomId); const roomPushRule = mx.getRoomPushRule('global', roomId);
const promises = []; const promises = [];
@ -76,7 +57,7 @@ function setRoomNotifType(roomId, newType) {
return promises; return promises;
} }
const oldState = getNotifType(roomId); const oldState = notifications.getNotiType(roomId);
if (oldState === cons.notifs.MUTE) { if (oldState === cons.notifs.MUTE) {
promises.push(mx.deletePushRule('global', 'override', roomId)); promises.push(mx.deletePushRule('global', 'override', roomId));
} }
@ -115,8 +96,9 @@ function setRoomNotifType(roomId, newType) {
} }
function useNotifications(roomId) { function useNotifications(roomId) {
const [activeType, setActiveType] = useState(getNotifType(roomId)); const { notifications } = initMatrix;
useEffect(() => setActiveType(getNotifType(roomId)), [roomId]); const [activeType, setActiveType] = useState(notifications.getNotiType(roomId));
useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]);
const setNotification = useCallback((item) => { const setNotification = useCallback((item) => {
if (item.type === activeType.type) return; if (item.type === activeType.type) return;

View file

@ -11,13 +11,16 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge';
import { blurOnBubbling } from '../../atoms/button/script'; import { blurOnBubbling } from '../../atoms/button/script';
function RoomSelectorWrapper({ function RoomSelectorWrapper({
isSelected, isUnread, onClick, isSelected, isMuted, isUnread, onClick,
content, options, onContextMenu, content, options, onContextMenu,
}) { }) {
let myClass = isUnread ? ' room-selector--unread' : ''; const classes = ['room-selector'];
myClass += isSelected ? ' room-selector--selected' : ''; if (isMuted) classes.push('room-selector--muted');
if (isUnread) classes.push('room-selector--unread');
if (isSelected) classes.push('room-selector--selected');
return ( return (
<div className={`room-selector${myClass}`}> <div className={classes.join(' ')}>
<button <button
className="room-selector__content" className="room-selector__content"
type="button" type="button"
@ -32,11 +35,13 @@ function RoomSelectorWrapper({
); );
} }
RoomSelectorWrapper.defaultProps = { RoomSelectorWrapper.defaultProps = {
isMuted: false,
options: null, options: null,
onContextMenu: null, onContextMenu: null,
}; };
RoomSelectorWrapper.propTypes = { RoomSelectorWrapper.propTypes = {
isSelected: PropTypes.bool.isRequired, isSelected: PropTypes.bool.isRequired,
isMuted: PropTypes.bool,
isUnread: PropTypes.bool.isRequired, isUnread: PropTypes.bool.isRequired,
onClick: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired,
content: PropTypes.node.isRequired, content: PropTypes.node.isRequired,
@ -46,12 +51,13 @@ RoomSelectorWrapper.propTypes = {
function RoomSelector({ function RoomSelector({
name, parentName, roomId, imageSrc, iconSrc, name, parentName, roomId, imageSrc, iconSrc,
isSelected, isUnread, notificationCount, isAlert, isSelected, isMuted, isUnread, notificationCount, isAlert,
options, onClick, onContextMenu, options, onClick, onContextMenu,
}) { }) {
return ( return (
<RoomSelectorWrapper <RoomSelectorWrapper
isSelected={isSelected} isSelected={isSelected}
isMuted={isMuted}
isUnread={isUnread} isUnread={isUnread}
content={( content={(
<> <>
@ -91,6 +97,7 @@ RoomSelector.defaultProps = {
isSelected: false, isSelected: false,
imageSrc: null, imageSrc: null,
iconSrc: null, iconSrc: null,
isMuted: false,
options: null, options: null,
onContextMenu: null, onContextMenu: null,
}; };
@ -101,6 +108,7 @@ RoomSelector.propTypes = {
imageSrc: PropTypes.string, imageSrc: PropTypes.string,
iconSrc: PropTypes.string, iconSrc: PropTypes.string,
isSelected: PropTypes.bool, isSelected: PropTypes.bool,
isMuted: PropTypes.bool,
isUnread: PropTypes.bool.isRequired, isUnread: PropTypes.bool.isRequired,
notificationCount: PropTypes.oneOfType([ notificationCount: PropTypes.oneOfType([
PropTypes.string, PropTypes.string,

View file

@ -9,6 +9,10 @@
border-radius: var(--bo-radius); border-radius: var(--bo-radius);
cursor: pointer; cursor: pointer;
&--muted {
opacity: 0.6;
}
&--unread { &--unread {
.room-selector__content > .text { .room-selector__content > .text {
color: var(--tc-surface-high); color: var(--tc-surface-high);

View file

@ -33,9 +33,11 @@ function Directs() {
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
return () => { return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
}; };
}, []); }, []);

View file

@ -62,9 +62,11 @@ function Home({ spaceId }) {
navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged);
return () => { return () => {
navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged);
notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged);
notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged);
}; };
}, []); }, []);

View file

@ -3,6 +3,7 @@ import React, { useEffect } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons';
import navigation from '../../../client/state/navigation'; import navigation from '../../../client/state/navigation';
import { openReusableContextMenu } from '../../../client/action/navigation'; import { openReusableContextMenu } from '../../../client/action/navigation';
import { getEventCords, abbreviateNumber } from '../../../util/common'; import { getEventCords, abbreviateNumber } from '../../../util/common';
@ -23,9 +24,12 @@ function Selector({
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const noti = initMatrix.notifications; const noti = initMatrix.notifications;
const room = mx.getRoom(roomId); const room = mx.getRoom(roomId);
let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; let imageSrc = room.getAvatarFallbackMember()?.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null; if (imageSrc === null) imageSrc = room.getAvatarUrl(mx.baseUrl, 24, 24, 'crop') || null;
const isMuted = noti.getNotiType(roomId) === cons.notifs.MUTE;
const [, forceUpdate] = useForceUpdate(); const [, forceUpdate] = useForceUpdate();
useEffect(() => { useEffect(() => {
@ -56,7 +60,8 @@ function Selector({
imageSrc={isDM ? imageSrc : null} imageSrc={isDM ? imageSrc : null}
iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())} iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())}
isSelected={navigation.selectedRoomId === roomId} isSelected={navigation.selectedRoomId === roomId}
isUnread={noti.hasNoti(roomId)} isMuted={isMuted}
isUnread={!isMuted && noti.hasNoti(roomId)}
notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))} notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))}
isAlert={noti.getHighlightNoti(roomId) !== 0} isAlert={noti.getHighlightNoti(roomId) !== 0}
onClick={onClick} onClick={onClick}

View file

@ -17,6 +17,17 @@ function isNotifEvent(mEvent) {
return true; return true;
} }
function isMutedRule(rule) {
return rule.actions[0] === 'dont_notify' && rule.conditions[0].kind === 'event_match';
}
function findMutedRule(overrideRules, roomId) {
return overrideRules.find((rule) => (
rule.rule_id === roomId
&& isMutedRule(rule)
));
}
class Notifications extends EventEmitter { class Notifications extends EventEmitter {
constructor(roomList) { constructor(roomList) {
super(); super();
@ -39,7 +50,9 @@ class Notifications extends EventEmitter {
_initNoti() { _initNoti() {
const addNoti = (roomId) => { const addNoti = (roomId) => {
const room = this.matrixClient.getRoom(roomId); const room = this.matrixClient.getRoom(roomId);
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return;
if (this.doesRoomHaveUnread(room) === false) return; if (this.doesRoomHaveUnread(room) === false) return;
const total = room.getUnreadNotificationCount('total'); const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight'); const highlight = room.getUnreadNotificationCount('highlight');
this._setNoti(room.roomId, total ?? 0, highlight ?? 0); this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
@ -65,6 +78,22 @@ class Notifications extends EventEmitter {
return true; return true;
} }
getNotiType(roomId) {
const mx = this.matrixClient;
const pushRule = mx.getRoomPushRule('global', roomId);
if (pushRule === undefined) {
const overrideRules = mx.getAccountData('m.push_rules')?.getContent()?.global?.override;
if (overrideRules === undefined) return cons.notifs.DEFAULT;
const isMuted = findMutedRule(overrideRules, roomId);
return isMuted ? cons.notifs.MUTE : cons.notifs.DEFAULT;
}
if (pushRule.actions[0] === 'notify') return cons.notifs.ALL_MESSAGES;
return cons.notifs.MENTIONS_AND_KEYWORDS;
}
getNoti(roomId) { getNoti(roomId) {
return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null }; return this.roomIdToNoti.get(roomId) || { total: 0, highlight: 0, from: null };
} }
@ -195,6 +224,7 @@ class Notifications extends EventEmitter {
this.matrixClient.on('Room.timeline', (mEvent, room) => { this.matrixClient.on('Room.timeline', (mEvent, room) => {
if (room.isSpaceRoom()) return; if (room.isSpaceRoom()) return;
if (!isNotifEvent(mEvent)) return; if (!isNotifEvent(mEvent)) return;
const liveEvents = room.getLiveTimeline().getEvents(); const liveEvents = room.getLiveTimeline().getEvents();
const lastTimelineEvent = liveEvents[liveEvents.length - 1]; const lastTimelineEvent = liveEvents[liveEvents.length - 1];
@ -204,6 +234,11 @@ class Notifications extends EventEmitter {
const total = room.getUnreadNotificationCount('total'); const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight'); const highlight = room.getUnreadNotificationCount('highlight');
if (this.getNotiType(room.roomId) === cons.notifs.MUTE) {
this.deleteNoti(room.roomId, total ?? 0, highlight ?? 0);
return;
}
this._setNoti(room.roomId, total ?? 0, highlight ?? 0); this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
if (this.matrixClient.getSyncState() === 'SYNCING') { if (this.matrixClient.getSyncState() === 'SYNCING') {
@ -211,6 +246,43 @@ class Notifications extends EventEmitter {
} }
}); });
this.matrixClient.on('accountData', (mEvent, oldMEvent) => {
if (mEvent.getType() === 'm.push_rules') {
const override = mEvent?.getContent()?.global?.override;
const oldOverride = oldMEvent?.getContent()?.global?.override;
if (!override || !oldOverride) return;
const isMuteToggled = (rule, otherOverride) => {
const roomId = rule.rule_id;
const room = this.matrixClient.getRoom(roomId);
if (room === null) return false;
if (room.isSpaceRoom()) return false;
const isMuted = isMutedRule(rule);
if (!isMuted) return false;
const isOtherMuted = findMutedRule(otherOverride, roomId);
if (isOtherMuted) return false;
return true;
};
const mutedRules = override.filter((rule) => isMuteToggled(rule, oldOverride));
const unMutedRules = oldOverride.filter((rule) => isMuteToggled(rule, override));
mutedRules.forEach((rule) => {
this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, true);
this.deleteNoti(rule.rule_id);
});
unMutedRules.forEach((rule) => {
this.emit(cons.events.notifications.MUTE_TOGGLED, rule.rule_id, false);
const room = this.matrixClient.getRoom(rule.rule_id);
if (!this.doesRoomHaveUnread(room)) return;
const total = room.getUnreadNotificationCount('total');
const highlight = room.getUnreadNotificationCount('highlight');
this._setNoti(room.roomId, total ?? 0, highlight ?? 0);
});
}
});
this.matrixClient.on('Room.receipt', (mEvent, room) => { this.matrixClient.on('Room.receipt', (mEvent, room) => {
if (mEvent.getType() === 'm.receipt') { if (mEvent.getType() === 'm.receipt') {
if (room.isSpaceRoom()) return; if (room.isSpaceRoom()) return;

View file

@ -107,6 +107,7 @@ const cons = {
notifications: { notifications: {
NOTI_CHANGED: 'NOTI_CHANGED', NOTI_CHANGED: 'NOTI_CHANGED',
FULL_READ: 'FULL_READ', FULL_READ: 'FULL_READ',
MUTE_TOGGLED: 'MUTE_TOGGLED',
}, },
roomTimeline: { roomTimeline: {
READY: 'READY', READY: 'READY',