Add option to change room notification settings (#2281)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Thu, 20 Mar 2025 09:27:00 +0000 (20:27 +1100)
committerGitHub <noreply@github.com>
Thu, 20 Mar 2025 09:27:00 +0000 (20:27 +1100)
12 files changed:
src/app/components/RoomNotificationSwitcher.tsx [new file with mode: 0644]
src/app/features/room-nav/RoomNavItem.tsx
src/app/hooks/useNotificationMode.ts
src/app/hooks/useRoomsNotificationPreferences.ts [new file with mode: 0644]
src/app/pages/Router.tsx
src/app/pages/client/ClientRoomsNotificationPreferences.tsx [new file with mode: 0644]
src/app/pages/client/direct/Direct.tsx
src/app/pages/client/home/Home.tsx
src/app/pages/client/space/Space.tsx
src/app/state/hooks/useBindAtoms.ts
src/app/state/room-list/mutedRoomList.ts [deleted file]
src/app/state/room/roomToUnread.ts

diff --git a/src/app/components/RoomNotificationSwitcher.tsx b/src/app/components/RoomNotificationSwitcher.tsx
new file mode 100644 (file)
index 0000000..7ce9156
--- /dev/null
@@ -0,0 +1,120 @@
+import { Box, config, Icon, Menu, MenuItem, PopOut, RectCords, Text } from 'folds';
+import React, { MouseEventHandler, ReactNode, useMemo, useState } from 'react';
+import FocusTrap from 'focus-trap-react';
+import { stopPropagation } from '../utils/keyboard';
+import {
+  getRoomNotificationModeIcon,
+  RoomNotificationMode,
+  useSetRoomNotificationPreference,
+} from '../hooks/useRoomsNotificationPreferences';
+import { AsyncStatus } from '../hooks/useAsyncCallback';
+
+const useRoomNotificationModes = (): RoomNotificationMode[] =>
+  useMemo(
+    () => [
+      RoomNotificationMode.Unset,
+      RoomNotificationMode.AllMessages,
+      RoomNotificationMode.SpecialMessages,
+      RoomNotificationMode.Mute,
+    ],
+    []
+  );
+
+const useRoomNotificationModeStr = (): Record<RoomNotificationMode, string> =>
+  useMemo(
+    () => ({
+      [RoomNotificationMode.Unset]: 'Default',
+      [RoomNotificationMode.AllMessages]: 'All Messages',
+      [RoomNotificationMode.SpecialMessages]: 'Mention & Keywords',
+      [RoomNotificationMode.Mute]: 'Mute',
+    }),
+    []
+  );
+
+type NotificationModeSwitcherProps = {
+  roomId: string;
+  value?: RoomNotificationMode;
+  children: (
+    handleOpen: MouseEventHandler<HTMLButtonElement>,
+    opened: boolean,
+    changing: boolean
+  ) => ReactNode;
+};
+export function RoomNotificationModeSwitcher({
+  roomId,
+  value = RoomNotificationMode.Unset,
+  children,
+}: NotificationModeSwitcherProps) {
+  const modes = useRoomNotificationModes();
+  const modeToStr = useRoomNotificationModeStr();
+
+  const { modeState, setMode } = useSetRoomNotificationPreference(roomId);
+  const changing = modeState.status === AsyncStatus.Loading;
+
+  const [menuCords, setMenuCords] = useState<RectCords>();
+
+  const handleOpenMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
+    setMenuCords(evt.currentTarget.getBoundingClientRect());
+  };
+
+  const handleClose = () => {
+    setMenuCords(undefined);
+  };
+
+  const handleSelect = (mode: RoomNotificationMode) => {
+    if (changing) return;
+    setMode(mode, value);
+    handleClose();
+  };
+
+  return (
+    <PopOut
+      anchor={menuCords}
+      offset={5}
+      position="Right"
+      align="Start"
+      content={
+        <FocusTrap
+          focusTrapOptions={{
+            initialFocus: false,
+            onDeactivate: handleClose,
+            clickOutsideDeactivates: true,
+            isKeyForward: (evt: KeyboardEvent) =>
+              evt.key === 'ArrowDown' || evt.key === 'ArrowRight',
+            isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft',
+            escapeDeactivates: stopPropagation,
+          }}
+        >
+          <Menu>
+            <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
+              {modes.map((mode) => (
+                <MenuItem
+                  key={mode}
+                  size="300"
+                  variant="Surface"
+                  aria-pressed={mode === value}
+                  radii="300"
+                  disabled={changing}
+                  onClick={() => handleSelect(mode)}
+                  before={
+                    <Icon
+                      size="100"
+                      src={getRoomNotificationModeIcon(mode)}
+                      filled={mode === value}
+                    />
+                  }
+                >
+                  <Text size="T300">
+                    {mode === value ? <b>{modeToStr[mode]}</b> : modeToStr[mode]}
+                  </Text>
+                </MenuItem>
+              ))}
+            </Box>
+          </Menu>
+        </FocusTrap>
+      }
+    >
+      {children(handleOpenMenu, !!menuCords, changing)}
+    </PopOut>
+  );
+}
index 27f728359ebd25f670a290a767f41b68a4f9b205..bdb8141850c84c9dd9c002cf4e93861709a82c63 100644 (file)
@@ -15,6 +15,7 @@ import {
   Line,
   RectCords,
   Badge,
+  Spinner,
 } from 'folds';
 import { useFocusWithin, useHover } from 'react-aria';
 import FocusTrap from 'focus-trap-react';
@@ -43,13 +44,19 @@ import { useSetting } from '../../state/hooks/settings';
 import { settingsAtom } from '../../state/settings';
 import { useOpenRoomSettings } from '../../state/hooks/roomSettings';
 import { useSpaceOptionally } from '../../hooks/useSpace';
+import {
+  getRoomNotificationModeIcon,
+  RoomNotificationMode,
+} from '../../hooks/useRoomsNotificationPreferences';
+import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher';
 
 type RoomNavItemMenuProps = {
   room: Room;
   requestClose: () => void;
+  notificationMode?: RoomNotificationMode;
 };
 const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
-  ({ room, requestClose }, ref) => {
+  ({ room, requestClose, notificationMode }, ref) => {
     const mx = useMatrixClient();
     const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
     const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
@@ -95,6 +102,27 @@ const RoomNavItemMenu = forwardRef<HTMLDivElement, RoomNavItemMenuProps>(
               Mark as Read
             </Text>
           </MenuItem>
+          <RoomNotificationModeSwitcher roomId={room.roomId} value={notificationMode}>
+            {(handleOpen, opened, changing) => (
+              <MenuItem
+                size="300"
+                after={
+                  changing ? (
+                    <Spinner size="100" variant="Secondary" />
+                  ) : (
+                    <Icon size="100" src={getRoomNotificationModeIcon(notificationMode)} />
+                  )
+                }
+                radii="300"
+                aria-pressed={opened}
+                onClick={handleOpen}
+              >
+                <Text style={{ flexGrow: 1 }} as="span" size="T300" truncate>
+                  Notifications
+                </Text>
+              </MenuItem>
+            )}
+          </RoomNotificationModeSwitcher>
         </Box>
         <Line variant="Surface" size="300" />
         <Box direction="Column" gap="100" style={{ padding: config.space.S100 }}>
@@ -170,7 +198,7 @@ type RoomNavItemProps = {
   room: Room;
   selected: boolean;
   linkPath: string;
-  muted?: boolean;
+  notificationMode?: RoomNotificationMode;
   showAvatar?: boolean;
   direct?: boolean;
 };
@@ -179,7 +207,7 @@ export function RoomNavItem({
   selected,
   showAvatar,
   direct,
-  muted,
+  notificationMode,
   linkPath,
 }: RoomNavItemProps) {
   const mx = useMatrixClient();
@@ -263,7 +291,9 @@ export function RoomNavItem({
                 <UnreadBadge highlight={unread.highlight > 0} count={unread.total} />
               </UnreadBadgeCenter>
             )}
-            {muted && !optionsVisible && <Icon size="50" src={Icons.BellMute} />}
+            {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && (
+              <Icon size="50" src={getRoomNotificationModeIcon(notificationMode)} />
+            )}
           </Box>
         </NavItemContent>
       </NavLink>
@@ -287,7 +317,11 @@ export function RoomNavItem({
                   escapeDeactivates: stopPropagation,
                 }}
               >
-                <RoomNavItemMenu room={room} requestClose={() => setMenuAnchor(undefined)} />
+                <RoomNavItemMenu
+                  room={room}
+                  requestClose={() => setMenuAnchor(undefined)}
+                  notificationMode={notificationMode}
+                />
               </FocusTrap>
             }
           >
index 1c2267e68b3d85acdd0a0f3fb9e564a42e9a1cc9..df90f6477058081d4a8f585fd04b2cbfde815436 100644 (file)
@@ -7,6 +7,19 @@ export enum NotificationMode {
   NotifyLoud = 'NotifyLoud',
 }
 
+export const getNotificationMode = (actions: PushRuleAction[]): NotificationMode => {
+  const soundTweak = actions.find(
+    (action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
+  );
+  const notify = actions.find(
+    (action) => typeof action === 'string' && action === PushRuleActionName.Notify
+  );
+
+  if (notify && soundTweak) return NotificationMode.NotifyLoud;
+  if (notify) return NotificationMode.Notify;
+  return NotificationMode.OFF;
+};
+
 export type NotificationModeOptions = {
   soundValue?: string;
   highlight?: boolean;
@@ -49,18 +62,7 @@ export const useNotificationModeActions = (
 };
 
 export const useNotificationActionsMode = (actions: PushRuleAction[]): NotificationMode => {
-  const mode: NotificationMode = useMemo(() => {
-    const soundTweak = actions.find(
-      (action) => typeof action === 'object' && action.set_tweak === TweakName.Sound
-    );
-    const notify = actions.find(
-      (action) => typeof action === 'string' && action === PushRuleActionName.Notify
-    );
-
-    if (notify && soundTweak) return NotificationMode.NotifyLoud;
-    if (notify) return NotificationMode.Notify;
-    return NotificationMode.OFF;
-  }, [actions]);
+  const mode: NotificationMode = useMemo(() => getNotificationMode(actions), [actions]);
 
   return mode;
 };
diff --git a/src/app/hooks/useRoomsNotificationPreferences.ts b/src/app/hooks/useRoomsNotificationPreferences.ts
new file mode 100644 (file)
index 0000000..ceea95f
--- /dev/null
@@ -0,0 +1,169 @@
+import { createContext, useCallback, useContext, useMemo } from 'react';
+import { ConditionKind, IPushRules, MatrixClient, PushRuleKind } from 'matrix-js-sdk';
+import { Icons, IconSrc } from 'folds';
+import { AccountDataEvent } from '../../types/matrix/accountData';
+import { useAccountData } from './useAccountData';
+import { isRoomId } from '../utils/matrix';
+import {
+  getNotificationMode,
+  getNotificationModeActions,
+  NotificationMode,
+} from './useNotificationMode';
+import { useAsyncCallback } from './useAsyncCallback';
+import { useMatrixClient } from './useMatrixClient';
+
+export type RoomsNotificationPreferences = {
+  mute: Set<string>;
+  specialMessages: Set<string>;
+  allMessages: Set<string>;
+};
+
+const RoomsNotificationPreferencesContext = createContext<RoomsNotificationPreferences | null>(
+  null
+);
+export const RoomsNotificationPreferencesProvider = RoomsNotificationPreferencesContext.Provider;
+
+export const useRoomsNotificationPreferencesContext = (): RoomsNotificationPreferences => {
+  const preferences = useContext(RoomsNotificationPreferencesContext);
+
+  if (!preferences) {
+    throw new Error('No RoomsNotificationPreferences provided!');
+  }
+
+  return preferences;
+};
+
+export const useRoomsNotificationPreferences = (): RoomsNotificationPreferences => {
+  const pushRules = useAccountData(AccountDataEvent.PushRules)?.getContent<IPushRules>();
+
+  const preferences: RoomsNotificationPreferences = useMemo(() => {
+    const global = pushRules?.global;
+    const room = global?.room ?? [];
+    const override = global?.override ?? [];
+
+    const pref: RoomsNotificationPreferences = {
+      mute: new Set(),
+      specialMessages: new Set(),
+      allMessages: new Set(),
+    };
+
+    override.forEach((rule) => {
+      if (isRoomId(rule.rule_id) && getNotificationMode(rule.actions) === NotificationMode.OFF) {
+        pref.mute.add(rule.rule_id);
+      }
+    });
+    room.forEach((rule) => {
+      if (getNotificationMode(rule.actions) === NotificationMode.OFF) {
+        pref.specialMessages.add(rule.rule_id);
+      }
+    });
+    room.forEach((rule) => {
+      if (getNotificationMode(rule.actions) !== NotificationMode.OFF) {
+        pref.allMessages.add(rule.rule_id);
+      }
+    });
+
+    return pref;
+  }, [pushRules]);
+
+  return preferences;
+};
+
+export enum RoomNotificationMode {
+  Unset = 'Unset',
+  Mute = 'Mute',
+  SpecialMessages = 'SpecialMessages',
+  AllMessages = 'AllMessages',
+}
+
+export const getRoomNotificationMode = (
+  preferences: RoomsNotificationPreferences,
+  roomId: string
+): RoomNotificationMode => {
+  if (preferences.mute.has(roomId)) {
+    return RoomNotificationMode.Mute;
+  }
+  if (preferences.specialMessages.has(roomId)) {
+    return RoomNotificationMode.SpecialMessages;
+  }
+  if (preferences.allMessages.has(roomId)) {
+    return RoomNotificationMode.AllMessages;
+  }
+
+  return RoomNotificationMode.Unset;
+};
+
+export const useRoomNotificationPreference = (
+  preferences: RoomsNotificationPreferences,
+  roomId: string
+): RoomNotificationMode =>
+  useMemo(() => getRoomNotificationMode(preferences, roomId), [preferences, roomId]);
+
+export const getRoomNotificationModeIcon = (mode?: RoomNotificationMode): IconSrc => {
+  if (mode === RoomNotificationMode.Mute) return Icons.BellMute;
+  if (mode === RoomNotificationMode.SpecialMessages) return Icons.BellPing;
+  if (mode === RoomNotificationMode.AllMessages) return Icons.BellRing;
+
+  return Icons.Bell;
+};
+
+export const setRoomNotificationPreference = async (
+  mx: MatrixClient,
+  roomId: string,
+  mode: RoomNotificationMode,
+  previousMode: RoomNotificationMode
+): Promise<void> => {
+  // remove the old preference
+  if (
+    previousMode === RoomNotificationMode.AllMessages ||
+    previousMode === RoomNotificationMode.SpecialMessages
+  ) {
+    await mx.deletePushRule('global', PushRuleKind.RoomSpecific, roomId);
+  }
+  if (previousMode === RoomNotificationMode.Mute) {
+    await mx.deletePushRule('global', PushRuleKind.Override, roomId);
+  }
+
+  // set new preference
+  if (mode === RoomNotificationMode.Unset) {
+    return;
+  }
+
+  if (mode === RoomNotificationMode.Mute) {
+    await mx.addPushRule('global', PushRuleKind.Override, roomId, {
+      conditions: [
+        {
+          kind: ConditionKind.EventMatch,
+          key: 'room_id',
+          pattern: roomId,
+        },
+      ],
+      actions: getNotificationModeActions(NotificationMode.OFF),
+    });
+    return;
+  }
+
+  await mx.addPushRule('global', PushRuleKind.RoomSpecific, roomId, {
+    actions:
+      mode === RoomNotificationMode.AllMessages
+        ? getNotificationModeActions(NotificationMode.NotifyLoud)
+        : getNotificationModeActions(NotificationMode.OFF),
+  });
+};
+
+export const useSetRoomNotificationPreference = (roomId: string) => {
+  const mx = useMatrixClient();
+
+  const [modeState, setMode] = useAsyncCallback(
+    useCallback(
+      (mode: RoomNotificationMode, previousMode: RoomNotificationMode) =>
+        setRoomNotificationPreference(mx, roomId, mode, previousMode),
+      [mx, roomId]
+    )
+  );
+
+  return {
+    modeState,
+    setMode,
+  };
+};
index ee934e57370e06cd080a5d59a2da10e361cf0e8c..76f0460f9608895dd64952911ebf94f4a6f8c7be 100644 (file)
@@ -59,6 +59,7 @@ import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager';
 import { ReceiveSelfDeviceVerification } from '../components/DeviceVerification';
 import { AutoRestoreBackupOnVerification } from '../components/BackupRestore';
 import { RoomSettingsRenderer } from '../features/room-settings';
+import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences';
 
 export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) => {
   const { hashRouter } = clientConfig;
@@ -111,22 +112,24 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize)
           <>
             <ClientRoot>
               <ClientInitStorageAtom>
-                <ClientBindAtoms>
-                  <ClientNonUIFeatures>
-                    <ClientLayout
-                      nav={
-                        <MobileFriendlyClientNav>
-                          <SidebarNav />
-                        </MobileFriendlyClientNav>
-                      }
-                    >
-                      <Outlet />
-                    </ClientLayout>
-                    <RoomSettingsRenderer />
-                    <ReceiveSelfDeviceVerification />
-                    <AutoRestoreBackupOnVerification />
-                  </ClientNonUIFeatures>
-                </ClientBindAtoms>
+                <ClientRoomsNotificationPreferences>
+                  <ClientBindAtoms>
+                    <ClientNonUIFeatures>
+                      <ClientLayout
+                        nav={
+                          <MobileFriendlyClientNav>
+                            <SidebarNav />
+                          </MobileFriendlyClientNav>
+                        }
+                      >
+                        <Outlet />
+                      </ClientLayout>
+                      <RoomSettingsRenderer />
+                      <ReceiveSelfDeviceVerification />
+                      <AutoRestoreBackupOnVerification />
+                    </ClientNonUIFeatures>
+                  </ClientBindAtoms>
+                </ClientRoomsNotificationPreferences>
               </ClientInitStorageAtom>
             </ClientRoot>
             <AuthRouteThemeManager />
diff --git a/src/app/pages/client/ClientRoomsNotificationPreferences.tsx b/src/app/pages/client/ClientRoomsNotificationPreferences.tsx
new file mode 100644 (file)
index 0000000..1ab0213
--- /dev/null
@@ -0,0 +1,15 @@
+import React, { ReactNode } from 'react';
+import {
+  RoomsNotificationPreferencesProvider,
+  useRoomsNotificationPreferences,
+} from '../../hooks/useRoomsNotificationPreferences';
+
+export function ClientRoomsNotificationPreferences({ children }: { children: ReactNode }) {
+  const preferences = useRoomsNotificationPreferences();
+
+  return (
+    <RoomsNotificationPreferencesProvider value={preferences}>
+      {children}
+    </RoomsNotificationPreferencesProvider>
+  );
+}
index 5e79921471854065ef285ccd669fca275b6f5a61..b6a8de1a0af9d326fc5e5163d7ddc644f7274f8d 100644 (file)
@@ -33,7 +33,6 @@ import { getCanonicalAliasOrRoomId } from '../../../utils/matrix';
 import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom';
 import { VirtualTile } from '../../../components/virtualizer';
 import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
-import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
 import { makeNavCategoryId } from '../../../state/closedNavCategories';
 import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
 import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
@@ -47,6 +46,10 @@ import { markAsRead } from '../../../../client/action/notifications';
 import { stopPropagation } from '../../../utils/keyboard';
 import { useSetting } from '../../../state/hooks/settings';
 import { settingsAtom } from '../../../state/settings';
+import {
+  getRoomNotificationMode,
+  useRoomsNotificationPreferencesContext,
+} from '../../../hooks/useRoomsNotificationPreferences';
 
 type DirectMenuProps = {
   requestClose: () => void;
@@ -167,8 +170,7 @@ export function Direct() {
   useNavToActivePathMapper('direct');
   const scrollRef = useRef<HTMLDivElement>(null);
   const directs = useDirectRooms();
-  const muteChanges = useAtomValue(muteChangesAtom);
-  const mutedRooms = muteChanges.added;
+  const notificationPreferences = useRoomsNotificationPreferencesContext();
   const roomToUnread = useAtomValue(roomToUnreadAtom);
 
   const selectedRoomId = useSelectedRoom();
@@ -254,7 +256,10 @@ export function Direct() {
                         showAvatar
                         direct
                         linkPath={getDirectRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
-                        muted={mutedRooms.includes(roomId)}
+                        notificationMode={getRoomNotificationMode(
+                          notificationPreferences,
+                          room.roomId
+                        )}
                       />
                     </VirtualTile>
                   );
index fa5e68ab148c5cf89df4cb8a66a7ba6b5ad93df7..af4164fda88990949d5c357b2e5f931892a842c2 100644 (file)
@@ -37,7 +37,6 @@ import { useHomeRooms } from './useHomeRooms';
 import { useMatrixClient } from '../../../hooks/useMatrixClient';
 import { VirtualTile } from '../../../components/virtualizer';
 import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
-import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
 import { makeNavCategoryId } from '../../../state/closedNavCategories';
 import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
 import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
@@ -50,6 +49,10 @@ import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCatego
 import { stopPropagation } from '../../../utils/keyboard';
 import { useSetting } from '../../../state/hooks/settings';
 import { settingsAtom } from '../../../state/settings';
+import {
+  getRoomNotificationMode,
+  useRoomsNotificationPreferencesContext,
+} from '../../../hooks/useRoomsNotificationPreferences';
 
 type HomeMenuProps = {
   requestClose: () => void;
@@ -199,8 +202,7 @@ export function Home() {
   useNavToActivePathMapper('home');
   const scrollRef = useRef<HTMLDivElement>(null);
   const rooms = useHomeRooms();
-  const muteChanges = useAtomValue(muteChangesAtom);
-  const mutedRooms = muteChanges.added;
+  const notificationPreferences = useRoomsNotificationPreferencesContext();
   const roomToUnread = useAtomValue(roomToUnreadAtom);
 
   const selectedRoomId = useSelectedRoom();
@@ -321,7 +323,10 @@ export function Home() {
                         room={room}
                         selected={selected}
                         linkPath={getHomeRoomPath(getCanonicalAliasOrRoomId(mx, roomId))}
-                        muted={mutedRooms.includes(roomId)}
+                        notificationMode={getRoomNotificationMode(
+                          notificationPreferences,
+                          room.roomId
+                        )}
                       />
                     </VirtualTile>
                   );
index 1714d8ee02bc41812ee32356b316a960d7d98506..737663ad0679f893bf279a96a453cbffad4d6bb0 100644 (file)
@@ -45,7 +45,6 @@ import {
 import { useSpace } from '../../../hooks/useSpace';
 import { VirtualTile } from '../../../components/virtualizer';
 import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav';
-import { muteChangesAtom } from '../../../state/room-list/mutedRoomList';
 import { makeNavCategoryId } from '../../../state/closedNavCategories';
 import { roomToUnreadAtom } from '../../../state/room/roomToUnread';
 import { useCategoryHandler } from '../../../hooks/useCategoryHandler';
@@ -71,6 +70,10 @@ import { getMatrixToRoom } from '../../../plugins/matrix-to';
 import { getViaServers } from '../../../plugins/via-servers';
 import { useSetting } from '../../../state/hooks/settings';
 import { settingsAtom } from '../../../state/settings';
+import {
+  getRoomNotificationMode,
+  useRoomsNotificationPreferencesContext,
+} from '../../../hooks/useRoomsNotificationPreferences';
 
 type SpaceMenuProps = {
   room: Room;
@@ -269,8 +272,7 @@ export function Space() {
   const roomToUnread = useAtomValue(roomToUnreadAtom);
   const allRooms = useAtomValue(allRoomsAtom);
   const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]);
-  const muteChanges = useAtomValue(muteChangesAtom);
-  const mutedRooms = muteChanges.added;
+  const notificationPreferences = useRoomsNotificationPreferencesContext();
 
   const selectedRoomId = useSelectedRoom();
   const lobbySelected = useSpaceLobbySelected(spaceIdOrAlias);
@@ -404,7 +406,7 @@ export function Space() {
                     showAvatar={mDirects.has(roomId)}
                     direct={mDirects.has(roomId)}
                     linkPath={getToLink(roomId)}
-                    muted={mutedRooms.includes(roomId)}
+                    notificationMode={getRoomNotificationMode(notificationPreferences, room.roomId)}
                   />
                 </VirtualTile>
               );
index 136c833f0972016b5d595bbe6bfbd50fdb6c0c67..d4572ff4cdef3c2cf439d85f94bbfef3e2b8cb3d 100644 (file)
@@ -2,7 +2,6 @@ import { MatrixClient } from 'matrix-js-sdk';
 import { allInvitesAtom, useBindAllInvitesAtom } from '../room-list/inviteList';
 import { allRoomsAtom, useBindAllRoomsAtom } from '../room-list/roomList';
 import { mDirectAtom, useBindMDirectAtom } from '../mDirectList';
-import { muteChangesAtom, mutedRoomsAtom, useBindMutedRoomsAtom } from '../room-list/mutedRoomList';
 import { roomToUnreadAtom, useBindRoomToUnreadAtom } from '../room/roomToUnread';
 import { roomToParentsAtom, useBindRoomToParentsAtom } from '../room/roomToParents';
 import { roomIdToTypingMembersAtom, useBindRoomIdToTypingMembersAtom } from '../typingMembers';
@@ -12,8 +11,7 @@ export const useBindAtoms = (mx: MatrixClient) => {
   useBindAllInvitesAtom(mx, allInvitesAtom);
   useBindAllRoomsAtom(mx, allRoomsAtom);
   useBindRoomToParentsAtom(mx, roomToParentsAtom);
-  useBindMutedRoomsAtom(mx, mutedRoomsAtom);
-  useBindRoomToUnreadAtom(mx, roomToUnreadAtom, muteChangesAtom);
+  useBindRoomToUnreadAtom(mx, roomToUnreadAtom);
 
   useBindRoomIdToTypingMembersAtom(mx, roomIdToTypingMembersAtom);
 };
diff --git a/src/app/state/room-list/mutedRoomList.ts b/src/app/state/room-list/mutedRoomList.ts
deleted file mode 100644 (file)
index cb56ec0..0000000
+++ /dev/null
@@ -1,98 +0,0 @@
-import { atom, 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';
-
-export type MutedRoomsUpdate =
-  | {
-      type: 'INITIALIZE';
-      addRooms: string[];
-    }
-  | {
-      type: 'UPDATE';
-      addRooms: string[];
-      removeRooms: string[];
-    };
-
-export const muteChangesAtom = atom<MuteChanges>({
-  added: [],
-  removed: [],
-});
-
-const baseMutedRoomsAtom = atom(new Set<string>());
-export const mutedRoomsAtom = atom<Set<string>, [MutedRoomsUpdate], undefined>(
-  (get) => get(baseMutedRoomsAtom),
-  (get, set, action) => {
-    const mutedRooms = new Set([...get(mutedRoomsAtom)]);
-    if (action.type === 'INITIALIZE') {
-      set(baseMutedRoomsAtom, new Set([...action.addRooms]));
-      set(muteChangesAtom, {
-        added: [...action.addRooms],
-        removed: [],
-      });
-      return;
-    }
-    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: typeof mutedRoomsAtom) => {
-  const setMuted = useSetAtom(mutedAtom);
-
-  useEffect(() => {
-    const overrideRules = mx.getAccountData('m.push_rules')?.getContent<IPushRules>()
-      ?.global?.override;
-    if (overrideRules) {
-      const mutedRooms = overrideRules.reduce<string[]>((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]);
-};
index bf99fe346c1c2391ce04088135d00d946356b70b..f7ef1b797f70b3504ef32c71c5577cd7b9a04dc4 100644 (file)
@@ -1,5 +1,5 @@
 import produce from 'immer';
-import { atom, useSetAtom, PrimitiveAtom, useAtomValue } from 'jotai';
+import { atom, useSetAtom } from 'jotai';
 import {
   IRoomTimelineData,
   MatrixClient,
@@ -11,7 +11,6 @@ import {
 import { ReceiptContent, ReceiptType } from 'matrix-js-sdk/lib/@types/read_receipts';
 import { useCallback, useEffect } from 'react';
 import {
-  MuteChanges,
   Membership,
   NotificationType,
   RoomToUnread,
@@ -25,11 +24,11 @@ import {
   getUnreadInfo,
   getUnreadInfos,
   isNotificationEvent,
-  roomHaveUnread,
 } from '../../utils/room';
 import { roomToParentsAtom } from './roomToParents';
 import { useStateEventCallback } from '../../hooks/useStateEventCallback';
 import { useSyncState } from '../../hooks/useSyncState';
+import { useRoomsNotificationPreferencesContext } from '../../hooks/useRoomsNotificationPreferences';
 
 export type RoomToUnreadAction =
   | {
@@ -167,13 +166,9 @@ export const roomToUnreadAtom = atom<RoomToUnread, [RoomToUnreadAction], undefin
   }
 );
 
-export const useBindRoomToUnreadAtom = (
-  mx: MatrixClient,
-  unreadAtom: typeof roomToUnreadAtom,
-  muteChangesAtom: PrimitiveAtom<MuteChanges>
-) => {
+export const useBindRoomToUnreadAtom = (mx: MatrixClient, unreadAtom: typeof roomToUnreadAtom) => {
   const setUnreadAtom = useSetAtom(unreadAtom);
-  const muteChanges = useAtomValue(muteChangesAtom);
+  const roomsNotificationPreferences = useRoomsNotificationPreferencesContext();
 
   useEffect(() => {
     setUnreadAtom({
@@ -249,16 +244,11 @@ export const useBindRoomToUnreadAtom = (
   }, [mx, setUnreadAtom]);
 
   useEffect(() => {
-    muteChanges.removed.forEach((roomId) => {
-      const room = mx.getRoom(roomId);
-      if (!room) return;
-      if (!roomHaveUnread(mx, room)) return;
-      setUnreadAtom({ type: 'PUT', unreadInfo: getUnreadInfo(room) });
-    });
-    muteChanges.added.forEach((roomId) => {
-      setUnreadAtom({ type: 'DELETE', roomId });
+    setUnreadAtom({
+      type: 'RESET',
+      unreadInfos: getUnreadInfos(mx),
     });
-  }, [mx, setUnreadAtom, muteChanges]);
+  }, [mx, setUnreadAtom, roomsNotificationPreferences]);
 
   useEffect(() => {
     const handleMembershipChange = (room: Room, membership: string) => {