diff --git a/src/app/molecules/room-notification/RoomNotification.jsx b/src/app/molecules/room-notification/RoomNotification.jsx index 5d118632..26d123bc 100644 --- a/src/app/molecules/room-notification/RoomNotification.jsx +++ b/src/app/molecules/room-notification/RoomNotification.jsx @@ -32,28 +32,9 @@ const items = [{ 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) { const mx = initMatrix.matrixClient; + const { notifications } = initMatrix; const roomPushRule = mx.getRoomPushRule('global', roomId); const promises = []; @@ -76,7 +57,7 @@ function setRoomNotifType(roomId, newType) { return promises; } - const oldState = getNotifType(roomId); + const oldState = notifications.getNotiType(roomId); if (oldState === cons.notifs.MUTE) { promises.push(mx.deletePushRule('global', 'override', roomId)); } @@ -115,8 +96,9 @@ function setRoomNotifType(roomId, newType) { } function useNotifications(roomId) { - const [activeType, setActiveType] = useState(getNotifType(roomId)); - useEffect(() => setActiveType(getNotifType(roomId)), [roomId]); + const { notifications } = initMatrix; + const [activeType, setActiveType] = useState(notifications.getNotiType(roomId)); + useEffect(() => setActiveType(notifications.getNotiType(roomId)), [roomId]); const setNotification = useCallback((item) => { if (item.type === activeType.type) return; diff --git a/src/app/molecules/room-selector/RoomSelector.jsx b/src/app/molecules/room-selector/RoomSelector.jsx index 8bd6c207..fa6daa9e 100644 --- a/src/app/molecules/room-selector/RoomSelector.jsx +++ b/src/app/molecules/room-selector/RoomSelector.jsx @@ -11,13 +11,16 @@ import NotificationBadge from '../../atoms/badge/NotificationBadge'; import { blurOnBubbling } from '../../atoms/button/script'; function RoomSelectorWrapper({ - isSelected, isUnread, onClick, + isSelected, isMuted, isUnread, onClick, content, options, onContextMenu, }) { - let myClass = isUnread ? ' room-selector--unread' : ''; - myClass += isSelected ? ' room-selector--selected' : ''; + const classes = ['room-selector']; + if (isMuted) classes.push('room-selector--muted'); + if (isUnread) classes.push('room-selector--unread'); + if (isSelected) classes.push('room-selector--selected'); + return ( - <div className={`room-selector${myClass}`}> + <div className={classes.join(' ')}> <button className="room-selector__content" type="button" @@ -32,11 +35,13 @@ function RoomSelectorWrapper({ ); } RoomSelectorWrapper.defaultProps = { + isMuted: false, options: null, onContextMenu: null, }; RoomSelectorWrapper.propTypes = { isSelected: PropTypes.bool.isRequired, + isMuted: PropTypes.bool, isUnread: PropTypes.bool.isRequired, onClick: PropTypes.func.isRequired, content: PropTypes.node.isRequired, @@ -46,12 +51,13 @@ RoomSelectorWrapper.propTypes = { function RoomSelector({ name, parentName, roomId, imageSrc, iconSrc, - isSelected, isUnread, notificationCount, isAlert, + isSelected, isMuted, isUnread, notificationCount, isAlert, options, onClick, onContextMenu, }) { return ( <RoomSelectorWrapper isSelected={isSelected} + isMuted={isMuted} isUnread={isUnread} content={( <> @@ -91,6 +97,7 @@ RoomSelector.defaultProps = { isSelected: false, imageSrc: null, iconSrc: null, + isMuted: false, options: null, onContextMenu: null, }; @@ -101,6 +108,7 @@ RoomSelector.propTypes = { imageSrc: PropTypes.string, iconSrc: PropTypes.string, isSelected: PropTypes.bool, + isMuted: PropTypes.bool, isUnread: PropTypes.bool.isRequired, notificationCount: PropTypes.oneOfType([ PropTypes.string, diff --git a/src/app/molecules/room-selector/RoomSelector.scss b/src/app/molecules/room-selector/RoomSelector.scss index 7ee8a3a2..59e47473 100644 --- a/src/app/molecules/room-selector/RoomSelector.scss +++ b/src/app/molecules/room-selector/RoomSelector.scss @@ -9,6 +9,10 @@ border-radius: var(--bo-radius); cursor: pointer; + &--muted { + opacity: 0.6; + } + &--unread { .room-selector__content > .text { color: var(--tc-surface-high); diff --git a/src/app/organisms/navigation/Directs.jsx b/src/app/organisms/navigation/Directs.jsx index 49244bc3..61313479 100644 --- a/src/app/organisms/navigation/Directs.jsx +++ b/src/app/organisms/navigation/Directs.jsx @@ -33,9 +33,11 @@ function Directs() { navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); + notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged); return () => { navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); + notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged); }; }, []); diff --git a/src/app/organisms/navigation/Home.jsx b/src/app/organisms/navigation/Home.jsx index c52ead49..35e43a97 100644 --- a/src/app/organisms/navigation/Home.jsx +++ b/src/app/organisms/navigation/Home.jsx @@ -62,9 +62,11 @@ function Home({ spaceId }) { navigation.on(cons.events.navigation.ROOM_SELECTED, selectorChanged); notifications.on(cons.events.notifications.NOTI_CHANGED, notiChanged); + notifications.on(cons.events.notifications.MUTE_TOGGLED, notiChanged); return () => { navigation.removeListener(cons.events.navigation.ROOM_SELECTED, selectorChanged); notifications.removeListener(cons.events.notifications.NOTI_CHANGED, notiChanged); + notifications.removeListener(cons.events.notifications.MUTE_TOGGLED, notiChanged); }; }, []); diff --git a/src/app/organisms/navigation/RoomsCategory.jsx b/src/app/organisms/navigation/RoomsCategory.jsx index f31e72f6..b5666512 100644 --- a/src/app/organisms/navigation/RoomsCategory.jsx +++ b/src/app/organisms/navigation/RoomsCategory.jsx @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import './RoomsCategory.scss'; import initMatrix from '../../../client/initMatrix'; -import { selectSpace, selectRoom,openReusableContextMenu } from '../../../client/action/navigation'; +import { selectSpace, selectRoom, openReusableContextMenu } from '../../../client/action/navigation'; import { getEventCords } from '../../../util/common'; import Text from '../../atoms/text/Text'; diff --git a/src/app/organisms/navigation/Selector.jsx b/src/app/organisms/navigation/Selector.jsx index 56e57c57..cb1086ea 100644 --- a/src/app/organisms/navigation/Selector.jsx +++ b/src/app/organisms/navigation/Selector.jsx @@ -3,6 +3,7 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import initMatrix from '../../../client/initMatrix'; +import cons from '../../../client/state/cons'; import navigation from '../../../client/state/navigation'; import { openReusableContextMenu } from '../../../client/action/navigation'; import { getEventCords, abbreviateNumber } from '../../../util/common'; @@ -23,9 +24,12 @@ function Selector({ const mx = initMatrix.matrixClient; const noti = initMatrix.notifications; const room = mx.getRoom(roomId); + let imageSrc = room.getAvatarFallbackMember()?.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(); useEffect(() => { @@ -56,7 +60,8 @@ function Selector({ imageSrc={isDM ? imageSrc : null} iconSrc={isDM ? null : joinRuleToIconSrc(room.getJoinRule(), room.isSpaceRoom())} isSelected={navigation.selectedRoomId === roomId} - isUnread={noti.hasNoti(roomId)} + isMuted={isMuted} + isUnread={!isMuted && noti.hasNoti(roomId)} notificationCount={abbreviateNumber(noti.getTotalNoti(roomId))} isAlert={noti.getHighlightNoti(roomId) !== 0} onClick={onClick} diff --git a/src/client/state/Notifications.js b/src/client/state/Notifications.js index 8ede1696..11fd665e 100644 --- a/src/client/state/Notifications.js +++ b/src/client/state/Notifications.js @@ -17,6 +17,17 @@ function isNotifEvent(mEvent) { 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 { constructor(roomList) { super(); @@ -39,7 +50,9 @@ class Notifications extends EventEmitter { _initNoti() { const addNoti = (roomId) => { const room = this.matrixClient.getRoom(roomId); + if (this.getNotiType(room.roomId) === cons.notifs.MUTE) return; if (this.doesRoomHaveUnread(room) === false) return; + const total = room.getUnreadNotificationCount('total'); const highlight = room.getUnreadNotificationCount('highlight'); this._setNoti(room.roomId, total ?? 0, highlight ?? 0); @@ -65,6 +78,22 @@ class Notifications extends EventEmitter { 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) { 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) => { if (room.isSpaceRoom()) return; if (!isNotifEvent(mEvent)) return; + const liveEvents = room.getLiveTimeline().getEvents(); const lastTimelineEvent = liveEvents[liveEvents.length - 1]; @@ -204,6 +234,11 @@ class Notifications extends EventEmitter { const total = room.getUnreadNotificationCount('total'); 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); 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) => { if (mEvent.getType() === 'm.receipt') { if (room.isSpaceRoom()) return; diff --git a/src/client/state/cons.js b/src/client/state/cons.js index 461b1892..c1f7f9ba 100644 --- a/src/client/state/cons.js +++ b/src/client/state/cons.js @@ -107,6 +107,7 @@ const cons = { notifications: { NOTI_CHANGED: 'NOTI_CHANGED', FULL_READ: 'FULL_READ', + MUTE_TOGGLED: 'MUTE_TOGGLED', }, roomTimeline: { READY: 'READY',