From 899a5b934ea898d49194278d393fdbe227ddcf97 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 31 Dec 2022 10:18:10 +0530 Subject: [PATCH] Add mute list atom --- src/app/state/mutedRoomList.ts | 93 +++++++++++++++++++++++++++++ src/app/utils/room.ts | 103 ++++++++++++++++++++++++++++++++- src/types/matrix/room.ts | 25 ++++++++ 3 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 src/app/state/mutedRoomList.ts diff --git a/src/app/state/mutedRoomList.ts b/src/app/state/mutedRoomList.ts new file mode 100644 index 00000000..a0ad66f3 --- /dev/null +++ b/src/app/state/mutedRoomList.ts @@ -0,0 +1,93 @@ +import { atom, WritableAtom, useSetAtom } from 'jotai'; +import { ClientEvent, IPushRule, IPushRules, MatrixClient, MatrixEvent } from 'matrix-js-sdk'; +import { useEffect } from 'react'; +import { MuteChanges } from '../../types/matrix/room'; +import { findMutedRule, isMutedRule } from '../utils/room'; + +type MutedRoomsUpdate = + | { + type: 'INITIALIZE'; + addRooms: string[]; + } + | { + type: 'UPDATE'; + addRooms: string[]; + removeRooms: string[]; + }; + +export const muteChangesAtom = atom({ + added: [], + removed: [], +}); + +const baseMutedRoomsAtom = atom(new Set()); +export const mutedRoomsAtom = atom, MutedRoomsUpdate>( + (get) => get(baseMutedRoomsAtom), + (get, set, action) => { + const mutedRooms = new Set([...get(mutedRoomsAtom)]); + if (action.type === 'UPDATE') { + action.removeRooms.forEach((roomId) => mutedRooms.delete(roomId)); + action.addRooms.forEach((roomId) => mutedRooms.add(roomId)); + set(baseMutedRoomsAtom, mutedRooms); + set(muteChangesAtom, { + added: [...action.addRooms], + removed: [...action.removeRooms], + }); + } + } +); + +export const useBindMutedRoomsAtom = ( + mx: MatrixClient, + mutedAtom: WritableAtom, MutedRoomsUpdate> +) => { + const setMuted = useSetAtom(mutedAtom); + + useEffect(() => { + const overrideRules = mx.getAccountData('m.push_rules')?.getContent() + ?.global?.override; + if (overrideRules) { + const mutedRooms = overrideRules.reduce((rooms, rule) => { + if (isMutedRule(rule)) rooms.push(rule.rule_id); + return rooms; + }, []); + setMuted({ + type: 'INITIALIZE', + addRooms: mutedRooms, + }); + } + }, [mx, setMuted]); + + useEffect(() => { + const handlePushRules = (mEvent: MatrixEvent, oldMEvent?: MatrixEvent) => { + if (mEvent.getType() === 'm.push_rules') { + const override = mEvent?.getContent()?.global?.override as IPushRule[] | undefined; + const oldOverride = oldMEvent?.getContent()?.global?.override as IPushRule[] | undefined; + if (!override || !oldOverride) return; + + const isMuteToggled = (rule: IPushRule, otherOverride: IPushRule[]) => { + const roomId = rule.rule_id; + + 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)); + + setMuted({ + type: 'UPDATE', + addRooms: mutedRules.map((rule) => rule.rule_id), + removeRooms: unMutedRules.map((rule) => rule.rule_id), + }); + } + }; + mx.on(ClientEvent.AccountData, handlePushRules); + return () => { + mx.removeListener(ClientEvent.AccountData, handlePushRules); + }; + }, [mx, setMuted]); +}; diff --git a/src/app/utils/room.ts b/src/app/utils/room.ts index 541ae2b7..d6563132 100644 --- a/src/app/utils/room.ts +++ b/src/app/utils/room.ts @@ -1,6 +1,19 @@ -import { MatrixClient, MatrixEvent, Room } from 'matrix-js-sdk'; +import { + IPushRule, + IPushRules, + MatrixClient, + MatrixEvent, + NotificationCountType, + Room, +} from 'matrix-js-sdk'; import { AccountDataEvent } from '../../types/matrix/accountData'; -import { RoomToParents, RoomType, StateEvent } from '../../types/matrix/room'; +import { + NotificationType, + RoomToParents, + RoomType, + StateEvent, + UnreadInfo, +} from '../../types/matrix/room'; export const getStateEvent = ( room: Room, @@ -116,3 +129,89 @@ export const getRoomToParents = (mx: MatrixClient): RoomToParents => { return map; }; + +export const isMutedRule = (rule: IPushRule) => + rule.actions[0] === 'dont_notify' && rule.conditions?.[0]?.kind === 'event_match'; + +export const findMutedRule = (overrideRules: IPushRule[], roomId: string) => + overrideRules.find((rule) => rule.rule_id === roomId && isMutedRule(rule)); + +export const getNotificationType = (mx: MatrixClient, roomId: string): NotificationType => { + let roomPushRule: IPushRule | undefined; + try { + roomPushRule = mx.getRoomPushRule('global', roomId); + } catch { + roomPushRule = undefined; + } + + if (!roomPushRule) { + const overrideRules = mx.getAccountData('m.push_rules')?.getContent() + ?.global?.override; + if (!overrideRules) return NotificationType.Default; + + return findMutedRule(overrideRules, roomId) ? NotificationType.Mute : NotificationType.Default; + } + + if (roomPushRule.actions[0] === 'notify') return NotificationType.AllMessages; + return NotificationType.MentionsAndKeywords; +}; + +export const isNotificationEvent = (mEvent: MatrixEvent) => { + const eType = mEvent.getType(); + if ( + ['m.room.create', 'm.room.message', 'm.room.encrypted', 'm.room.member', 'm.sticker'].find( + (type) => type === eType + ) + ) + return false; + if (eType === 'm.room.member') return false; + + if (mEvent.isRedacted()) return false; + if (mEvent.getRelation()?.rel_type === 'm.replace') return false; + + return true; +}; + +export const roomHaveUnread = (mx: MatrixClient, room: Room) => { + const userId = mx.getUserId(); + if (!userId) return false; + const readUpToId = room.getEventReadUpTo(userId); + const liveEvents = room.getLiveTimeline().getEvents(); + + if (liveEvents[liveEvents.length - 1]?.getSender() === userId) { + return false; + } + + for (let i = liveEvents.length - 1; i >= 0; i -= 1) { + const event = liveEvents[i]; + if (!event) return false; + if (event.getId() === readUpToId) return false; + if (isNotificationEvent(event)) return true; + } + return true; +}; + +export const getUnreadInfo = (room: Room): UnreadInfo => { + const total = room.getUnreadNotificationCount(NotificationCountType.Total); + const highlight = room.getUnreadNotificationCount(NotificationCountType.Highlight); + return { + roomId: room.roomId, + highlight, + total: highlight > total ? highlight : total, + }; +}; + +export const getUnreadInfos = (mx: MatrixClient): UnreadInfo[] => { + const unreadInfos = mx.getRooms().reduce((unread, room) => { + if (room.isSpaceRoom()) return unread; + if (room.getMyMembership() !== 'join') return unread; + if (getNotificationType(mx, room.roomId) === NotificationType.Mute) return unread; + + if (roomHaveUnread(mx, room)) { + unread.push(getUnreadInfo(room)); + } + + return unread; + }, []); + return unreadInfos; +}; diff --git a/src/types/matrix/room.ts b/src/types/matrix/room.ts index 0d8a8714..af795e06 100644 --- a/src/types/matrix/room.ts +++ b/src/types/matrix/room.ts @@ -31,4 +31,29 @@ export enum RoomType { Space = 'm.space', } +export enum NotificationType { + Default = 'default', + AllMessages = 'all_messages', + MentionsAndKeywords = 'mentions_and_keywords', + Mute = 'mute', +} + export type RoomToParents = Map>; +export type RoomToUnread = Map< + string, + { + total: number; + highlight: number; + from: Set | null; + } +>; +export type UnreadInfo = { + roomId: string; + total: number; + highlight: number; +}; + +export type MuteChanges = { + added: string[]; + removed: string[]; +};