diff --git a/.changeset/add_basic_hiding_rooms.md b/.changeset/add_basic_hiding_rooms.md new file mode 100644 index 000000000..6e20d1371 --- /dev/null +++ b/.changeset/add_basic_hiding_rooms.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Add per Space setting for when to show room icons in sidebar diff --git a/.changeset/fix-various-banner-fixes.md b/.changeset/fix-various-banner-fixes.md index 0c012d451..e75ee37fb 100644 --- a/.changeset/fix-various-banner-fixes.md +++ b/.changeset/fix-various-banner-fixes.md @@ -1,5 +1,5 @@ --- -default: fix +default: patch --- Various small banner changes diff --git a/src/app/components/GlobalKeyboardShortcuts.tsx b/src/app/components/GlobalKeyboardShortcuts.tsx index 7246219a4..675efd303 100644 --- a/src/app/components/GlobalKeyboardShortcuts.tsx +++ b/src/app/components/GlobalKeyboardShortcuts.tsx @@ -22,6 +22,10 @@ import { getCanonicalAliasOrRoomId } from '$utils/matrix'; import { announce } from '$utils/announce'; import { roomIdToReplyDraftAtomFamily } from '$state/room/roomInputDrafts'; import type { Room } from '$types/matrix-sdk'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { allRoomsAtom } from '$state/room-list/roomList'; +import { useSelectedRoom } from '$hooks/router/useSelectedRoom'; export function GlobalKeyboardShortcuts() { const navigate = useNavigate(); @@ -32,6 +36,16 @@ export function GlobalKeyboardShortcuts() { const roomToUnread = useAtomValue(roomToUnreadAtom); const unreadIndexRef = useRef(0); + const allRooms = useAtomValue(allRoomsAtom); + const [isHidingRooms, setIsHidingRooms] = useSetting(settingsAtom, 'isHidingRooms'); + const [hiddenRooms] = useSetting(settingsAtom, 'hiddenRooms'); + const [hiddenSpaces] = useSetting(settingsAtom, 'hiddenSpaces'); + const selectedRoomId = useSelectedRoom(); + + const filteredRooms = allRooms.filter( + (item) => !hiddenRooms.includes(item) && !hiddenSpaces.includes(item) + ); + // Derive the current room ID from the URL so we know which room is active. const roomMatch = matchPath(HOME_ROOM_PATH, location.pathname) ?? @@ -54,7 +68,7 @@ export function GlobalKeyboardShortcuts() { /** Navigate to a room by ID and announce it to screen readers. */ const navigateToRoom = useCallback( - (roomId: string, remaining: number) => { + (roomId: string, remaining?: number) => { const roomIdOrAliasToNav = getCanonicalAliasOrRoomId(mx, roomId); const isDirect = mDirects.has(roomId); if (isDirect) { @@ -75,7 +89,9 @@ export function GlobalKeyboardShortcuts() { } const roomName = mx.getRoom(roomId)?.name ?? 'Room'; const roomType = isDirect ? 'Direct Message' : 'Group Room'; - announce(`${roomName}, ${roomType}. ${remaining} room${remaining === 1 ? '' : 's'} unread.`); + announce( + `${roomName}, ${roomType}.${remaining && `${remaining} room${remaining === 1 ? '' : 's'} unread.`}` + ); }, [mx, mDirects, roomToParents, navigate] ); @@ -151,9 +167,28 @@ export function GlobalKeyboardShortcuts() { [currentRoom, replyDraft, setReplyDraft] ); + /** Alt+Shift+H: Toggle Hide Rooms. */ + const handleHideRoomsKeyDown = useCallback( + (evt: KeyboardEvent) => { + if (!isKeyHotkey('alt+shift+h', evt)) return; + evt.preventDefault(); + announce(`${isHidingRooms ? 'Disabling' : 'Enabling'} hiding rooms.`); + setIsHidingRooms(!isHidingRooms); + if ( + selectedRoomId && + filteredRooms.length > 0 && + filteredRooms[0] && + !filteredRooms.includes(selectedRoomId) + ) + navigateToRoom(filteredRooms[0]); + }, + [setIsHidingRooms, isHidingRooms, navigateToRoom, filteredRooms, selectedRoomId] + ); + useKeyDown(window, handleNextUnreadKeyDown); useKeyDown(window, handleUnreadNavKeyDown); useKeyDown(window, handleReplyKeyDown); + useKeyDown(window, handleHideRoomsKeyDown); return null; } diff --git a/src/app/components/user-profile/UserChips.tsx b/src/app/components/user-profile/UserChips.tsx index 2adeb5fbe..2abb69961 100644 --- a/src/app/components/user-profile/UserChips.tsx +++ b/src/app/components/user-profile/UserChips.tsx @@ -50,6 +50,8 @@ import { SettingTile } from '$components/setting-tile'; import { RoomAvatar, RoomIcon } from '$components/room-avatar'; import { heroMenuItemStyle } from './heroMenuItemStyle'; import * as css from './styles.css'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; export function ServerChip({ server, @@ -368,6 +370,21 @@ export function MutualRoomsChip({ const [cords, setCords] = useState(); + const [isHidingRooms] = useSetting(settingsAtom, 'isHidingRooms'); + const [hiddenRooms] = useSetting(settingsAtom, 'hiddenRooms'); + const [hiddenSpaces] = useSetting(settingsAtom, 'hiddenSpaces'); + const baseMutualRooms = useMemo( + () => + (mutualRoomsState.status === AsyncStatus.Success && + (!isHidingRooms + ? mutualRoomsState.data + : mutualRoomsState.data.filter( + (item) => !hiddenRooms.includes(item) && !hiddenSpaces.includes(item) + ))) || + [], + [isHidingRooms, hiddenRooms, hiddenSpaces, mutualRoomsState] + ); + const open: MouseEventHandler = (evt) => { setCords(evt.currentTarget.getBoundingClientRect()); }; @@ -382,7 +399,7 @@ export function MutualRoomsChip({ }; if (mutualRoomsState.status === AsyncStatus.Success) { - const mutualRooms = mutualRoomsState.data + const mutualRooms = baseMutualRooms .toSorted(factoryRoomIdByAtoZ(mx)) .map(getRoom) .filter((room) => !!room); @@ -399,7 +416,7 @@ export function MutualRoomsChip({ }); } return data; - }, [mutualRoomsState, getRoom, directs, mx]); + }, [mutualRoomsState, getRoom, directs, mx, baseMutualRooms]); if ( userId === mx.getSafeUserId() || @@ -541,9 +558,7 @@ export function MutualRoomsChip({ variant={cardColor ? undefined : 'SurfaceVariant'} radii="Pill" before={mutualRoomsState.status === AsyncStatus.Loading && } - disabled={ - mutualRoomsState.status !== AsyncStatus.Success || mutualRoomsState.data.length === 0 - } + disabled={mutualRoomsState.status !== AsyncStatus.Success || baseMutualRooms.length === 0} onClick={open} aria-pressed={!!cords} className={cardColor ? css.UserHeroChipThemed : css.UserHeroBrightnessHover} @@ -554,7 +569,7 @@ export function MutualRoomsChip({ > {mutualRoomsState.status === AsyncStatus.Success && - `${mutualRoomsState.data.length} Mutual Rooms`} + `${baseMutualRooms.length} Mutual Rooms`} {mutualRoomsState.status === AsyncStatus.Loading && 'Mutual Rooms'} diff --git a/src/app/features/common-settings/appearance/Appearance.tsx b/src/app/features/common-settings/appearance/Appearance.tsx index b7bc803e0..ee4715720 100644 --- a/src/app/features/common-settings/appearance/Appearance.tsx +++ b/src/app/features/common-settings/appearance/Appearance.tsx @@ -12,6 +12,7 @@ import { MenuItem, PopOut, type RectCords, + Switch, } from 'folds'; import { Page, PageContent, PageHeader } from '$components/page'; import { SequenceCard } from '$components/sequence-card'; @@ -98,11 +99,35 @@ export function SelectShowPerRoomRoomIcon({ roomId }: { roomId: string }) { ); } +export function SelectHideRoom({ roomId, isSpace }: { roomId: string; isSpace?: boolean }) { + const [hideRoom, setHideRoom] = useSetting( + settingsAtom, + isSpace ? 'hiddenSpaces' : 'hiddenRooms' + ); + const isHidden = hideRoom.includes(roomId); + + function handleHideRoom() { + const newHideRoomList = !isHidden + ? [...hideRoom, roomId] + : hideRoom.filter((roomItem) => roomItem !== roomId); + setHideRoom(newHideRoomList); + } + + return ( + } + /> + ); +} + type AppearanceProps = { requestClose: () => void; }; export function Appearance({ requestClose }: AppearanceProps) { const room = useRoom(); + const isSpace = room.isSpaceRoom(); return ( @@ -126,16 +151,25 @@ export function Appearance({ requestClose }: AppearanceProps) { Visual Tweaks + {isSpace && ( + + } + /> + + )} - } - /> + diff --git a/src/app/features/room-settings/RoomSettings.tsx b/src/app/features/room-settings/RoomSettings.tsx index f960460fd..228e757b3 100644 --- a/src/app/features/room-settings/RoomSettings.tsx +++ b/src/app/features/room-settings/RoomSettings.tsx @@ -23,6 +23,7 @@ import { useSetting } from '$state/hooks/settings'; import { Permissions } from './permissions'; import { General } from './general'; import { RoomAbbreviations } from './abbreviations/RoomAbbreviations'; +import { Appearance } from '$features/common-settings/appearance/Appearance'; type RoomSettingsMenuItem = { page: RoomSettingsPage; @@ -70,6 +71,12 @@ const useRoomSettingsMenuItems = (): RoomSettingsMenuItem[] => name: 'Developer Tools', icon: Icons.Terminal, }, + { + page: RoomSettingsPage.AppearancePage, + name: 'Appearance', + icon: Icons.Alphabet, + activeIcon: Icons.AlphabetUnderline, + }, ], [] ); @@ -209,6 +216,9 @@ export function RoomSettings({ initialPage, requestClose }: RoomSettingsProps) { {activePage === RoomSettingsPage.AbbreviationsPage && ( )} + {activePage === RoomSettingsPage.AppearancePage && ( + + )} ); diff --git a/src/app/features/settings/cosmetics/Themes.tsx b/src/app/features/settings/cosmetics/Themes.tsx index fa93a3890..de1ab971e 100644 --- a/src/app/features/settings/cosmetics/Themes.tsx +++ b/src/app/features/settings/cosmetics/Themes.tsx @@ -6,6 +6,7 @@ import { Chip, config, Icon, + IconButton, Icons, Input, Menu, @@ -787,6 +788,9 @@ export function Appearance({ const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [customDMCards, setCustomDMCards] = useSetting(settingsAtom, 'customDMCards'); const [showEasterEggs, setShowEasterEggs] = useSetting(settingsAtom, 'showEasterEggs'); + const [isHidingRooms, setIsHidingRooms] = useSetting(settingsAtom, 'isHidingRooms'); + const [hiddenSpaces, setHiddenSpaces] = useSetting(settingsAtom, 'hiddenSpaces'); + const [hiddenRooms, setHiddenRooms] = useSetting(settingsAtom, 'hiddenRooms'); const [themeBrowserOpen, setThemeBrowserOpen] = useState(false); const [closeFoldersByDefault, setCloseFoldersByDefault] = useSetting( settingsAtom, @@ -855,6 +859,38 @@ export function Appearance({ /> + + + } + /> + + + + { + setHiddenRooms([]); + setHiddenSpaces([]); + }} + radii="300" + disabled={hiddenRooms?.length === 0 && hiddenSpaces?.length === 0} + > + + + } + /> + + } /> diff --git a/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx b/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx index a0b52da48..aac71a6cd 100644 --- a/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx +++ b/src/app/features/settings/keyboard-shortcuts/KeyboardShortcuts.tsx @@ -46,6 +46,10 @@ const SHORTCUT_CATEGORIES: ShortcutCategory[] = [ { keys: 'Ctrl+U / ⌘+U', description: 'Underline' }, ], }, + { + name: 'Other', + shortcuts: [{ keys: 'Alt+Shift+H', description: 'Toggle Hiding Rooms' }], + }, ]; function ShortcutRow({ keys, description }: ShortcutEntry) { diff --git a/src/app/features/settings/settingsLink.ts b/src/app/features/settings/settingsLink.ts index 6104186ed..38f31f797 100644 --- a/src/app/features/settings/settingsLink.ts +++ b/src/app/features/settings/settingsLink.ts @@ -116,6 +116,8 @@ const settingsLinkFocusIdsBySection: Record { setCurWidth(roomSidebarWidth); }, [roomSidebarWidth]); @@ -233,7 +235,8 @@ export function Direct() { const sortedDirects = useMemo(() => { void activityCounter; - const items = Array.from(directs).toSorted(factoryRoomIdByActivity(mx)); + let items = Array.from(directs).toSorted(factoryRoomIdByActivity(mx)); + if (isHidingRooms) items = items.filter((rId) => !hiddenRooms.includes(rId)); const hasUnread = (roomId: string) => { const unread = roomToUnread.get(roomId); return !!unread && (unread.total > 0 || unread.highlight > 0); @@ -242,7 +245,16 @@ export function Direct() { return items.filter((rId) => hasUnread(rId) || rId === selectedRoomId); } return items; - }, [mx, directs, closedCategories, roomToUnread, selectedRoomId, activityCounter]); + }, [ + mx, + directs, + closedCategories, + roomToUnread, + selectedRoomId, + activityCounter, + hiddenRooms, + isHidingRooms, + ]); const virtualizer = useVirtualizer({ count: sortedDirects.length, diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index cddcefbb0..37c93d996 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -211,6 +211,9 @@ export function Home() { const roomToUnread = useAtomValue(roomToUnreadAtom); const navigate = useNavigate(); + const [hiddenRooms] = useSetting(settingsAtom, 'hiddenRooms'); + const [isHidingRooms] = useSetting(settingsAtom, 'isHidingRooms'); + const [roomSidebarWidth, setRoomSidebarWidth] = useSetting(settingsAtom, 'roomSidebarWidth'); const [curWidth, setCurWidth] = useState(roomSidebarWidth); useEffect(() => { @@ -236,11 +239,13 @@ export function Home() { const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); const sortedRooms = useMemo(() => { - const items = Array.from(rooms).toSorted( + let items = Array.from(rooms).toSorted( closedCategories.has(DEFAULT_CATEGORY_ID) ? factoryRoomIdByActivity(mx) : factoryRoomIdByAtoZ(mx) ); + + if (isHidingRooms) items = items.filter((rId) => !hiddenRooms.includes(rId)); const hasUnread = (roomId: string) => { const unread = roomToUnread.get(roomId); return !!unread && (unread.total > 0 || unread.highlight > 0); @@ -249,7 +254,7 @@ export function Home() { return items.filter((rId) => hasUnread(rId) || rId === selectedRoomId); } return items; - }, [mx, rooms, closedCategories, roomToUnread, selectedRoomId]); + }, [mx, rooms, closedCategories, roomToUnread, selectedRoomId, hiddenRooms, isHidingRooms]); const virtualizer = useVirtualizer({ count: sortedRooms.length, diff --git a/src/app/pages/client/sidebar/SpaceTabs.tsx b/src/app/pages/client/sidebar/SpaceTabs.tsx index e9e2a6e5a..34c01a273 100644 --- a/src/app/pages/client/sidebar/SpaceTabs.tsx +++ b/src/app/pages/client/sidebar/SpaceTabs.tsx @@ -753,6 +753,9 @@ export function SpaceTabs({ scrollRef }: Readonly) { anchor: RectCords; }>(); const [renameTargetFolder, setRenameTargetFolder] = useState(); + const [baseHiddenSpaces] = useSetting(settingsAtom, 'hiddenSpaces'); + const [isHidingRooms] = useSetting(settingsAtom, 'isHidingRooms'); + const hiddenSpaces = isHidingRooms ? baseHiddenSpaces : undefined; const handleFolderContextMenu = useCallback( (folder: ISidebarFolder): MouseEventHandler => @@ -957,7 +960,24 @@ export function SpaceTabs({ scrollRef }: Readonly) { [mx, sidebarItems, orphanSpaces, localEchoSidebarItem] ); - if (sidebarItems.length === 0) return null; + //This is a negative filter, so everything that IS inside of the 'filter' argument gets filtered OUT of the list + function handleFilterSpaces(list: SidebarItems, filter: string[] | undefined) { + if (filter?.length === 0) return list; + let spaces: TSidebarItem[] = []; + list.forEach((item) => { + if (typeof item === 'object') { + const filterSubSpaces = item.content.filter((sId) => !filter?.includes(sId)); + if (filterSubSpaces.length) { + let folder = item; + folder.content = filterSubSpaces; + spaces.push(folder); + } + } else if (!filter?.includes(item)) spaces.push(item); + }); + return spaces; + } + const filteredSidebarItems = handleFilterSpaces(sidebarItems, hiddenSpaces); + if (filteredSidebarItems.length === 0) return null; return ( <> {folderMenuState && ( @@ -998,7 +1018,7 @@ export function SpaceTabs({ scrollRef }: Readonly) { )} - {sidebarItems.map((item) => { + {filteredSidebarItems.map((item) => { if (typeof item === 'object') { if (openedFolder.has(item.id)) { return ( diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index e30634310..93c94d054 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -518,6 +518,9 @@ export function Space() { const allJoinedRooms = useMemo(() => new Set(allRooms), [allRooms]); const notificationPreferences = useRoomsNotificationPreferencesContext(); + const [hiddenRooms] = useSetting(settingsAtom, 'hiddenRooms'); + const [isHidingRooms] = useSetting(settingsAtom, 'isHidingRooms'); + const [roomSidebarWidth, setRoomSidebarWidth] = useSetting(settingsAtom, 'roomSidebarWidth'); const [curWidth, setCurWidth] = useState(roomSidebarWidth); useEffect(() => { @@ -780,7 +783,7 @@ export function Space() { ); }; - const hierarchy = useSpaceJoinedHierarchy( + const baseHierarchy = useSpaceJoinedHierarchy( space.roomId, getRoom, useCallback( @@ -814,6 +817,11 @@ export function Space() { [getInClosedCategories, space.roomId] ) ); + const hierarchy = isHidingRooms + ? baseHierarchy.filter( + (item) => !hiddenRooms.includes(item.roomId) || item.roomId === selectedRoomId + ) + : baseHierarchy; const virtualizer = useVirtualizer({ count: hierarchy.length, diff --git a/src/app/state/roomSettings.ts b/src/app/state/roomSettings.ts index 64bc0ef99..2b93307b9 100644 --- a/src/app/state/roomSettings.ts +++ b/src/app/state/roomSettings.ts @@ -9,6 +9,7 @@ export enum RoomSettingsPage { // Sable pages CosmeticsPage, AbbreviationsPage, + AppearancePage, } export type RoomSettingsState = { diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index aab58f600..e0e18e945 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -175,6 +175,9 @@ export interface Settings { threadRootHeight: number; vcmsgSidebarWidth: number; widgetSidebarWidth: number; + hiddenSpaces: string[]; + hiddenRooms: string[]; + isHidingRooms: boolean; // furry stuff renderAnimals: boolean; @@ -308,6 +311,10 @@ export const defaultSettings: Settings = { threadRootHeight: 220, vcmsgSidebarWidth: 399, widgetSidebarWidth: 420, + hiddenSpaces: [], + hiddenRooms: [], + isHidingRooms: false, + // furry stuff renderAnimals: true,