diff --git a/src/app/components/UserQuickToolsProvider.tsx b/src/app/components/UserQuickToolsProvider.tsx new file mode 100644 index 0000000000..23cf8c71de --- /dev/null +++ b/src/app/components/UserQuickToolsProvider.tsx @@ -0,0 +1,9 @@ +import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { UserQuickTools } from '$pages/client/sidebar/UserQuickTools'; + +export function UserQuickToolsProvider() { + const screenSize = useScreenSizeContext(); + const compact = screenSize === ScreenSize.Mobile; + if (!compact) return null; + return ; +} diff --git a/src/app/components/message/modals/GlobalModalManager.tsx b/src/app/components/message/modals/GlobalModalManager.tsx index be48d49be0..a78506cad7 100644 --- a/src/app/components/message/modals/GlobalModalManager.tsx +++ b/src/app/components/message/modals/GlobalModalManager.tsx @@ -34,7 +34,11 @@ export function GlobalModalManager() { focusTrapOptions={{ initialFocus: false, onDeactivate: close, - clickOutsideDeactivates: true, + allowOutsideClick: (e) => { + e.preventDefault(); + close(); + return false; + }, escapeDeactivates: stopPropagation, }} > diff --git a/src/app/components/message/modals/MessageForward.tsx b/src/app/components/message/modals/MessageForward.tsx index 946f764a0b..aef10a95cb 100644 --- a/src/app/components/message/modals/MessageForward.tsx +++ b/src/app/components/message/modals/MessageForward.tsx @@ -17,7 +17,7 @@ import * as Sentry from '@sentry/react'; import { isRoomPrivate } from '$utils/roomVisibility'; import { canForwardEvent } from '$utils/room'; import * as prefix from '$unstable/prefixes'; -import { RoomSearchModal } from '$features/search'; +import { SearchWrapper } from '$features/navigate'; const debugLog = createDebugLogger('MessageForward'); // Message forwarding component @@ -270,7 +270,7 @@ export function MessageForwardInternal({ if (!forwardable) return null; - return ; + return ; } type MessageForwardItemProps = { diff --git a/src/app/components/sidebar/Sidebar.css.ts b/src/app/components/sidebar/Sidebar.css.ts index f97c0f9fca..db345edaab 100644 --- a/src/app/components/sidebar/Sidebar.css.ts +++ b/src/app/components/sidebar/Sidebar.css.ts @@ -132,7 +132,7 @@ export const SidebarItemBottom = recipe({ selectors: { '&:hover': { - transform: `translateY(${toRem(PUSH_Y)})`, + transform: `translateY(${toRem(-PUSH_Y)})`, }, '&::before': { content: '', diff --git a/src/app/features/search/Search.tsx b/src/app/features/navigate/NavigateModal.tsx similarity index 50% rename from src/app/features/search/Search.tsx rename to src/app/features/navigate/NavigateModal.tsx index c6acf0d2a7..8da2f7a3df 100644 --- a/src/app/features/search/Search.tsx +++ b/src/app/features/navigate/NavigateModal.tsx @@ -131,7 +131,7 @@ export type RoomSearchPickRoomConfig = { }; export type RoomSearchModalProps = { - requestClose: () => void; + requestClose?: () => void; pickRoom?: RoomSearchPickRoomConfig; }; @@ -139,7 +139,6 @@ export function RoomSearchModal({ requestClose, pickRoom }: RoomSearchModalProps const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const scrollRef = useRef(null); - const inputRef = useRef(null); const { navigateRoom, navigateSpace } = useRoomNavigate(); const roomToUnread = useAtomValue(roomToUnreadAtom); @@ -214,7 +213,7 @@ export function RoomSearchModal({ requestClose, pickRoom }: RoomSearchModalProps } if (isSpace) navigateSpace(roomId); else navigateRoom(roomId); - requestClose(); + requestClose?.(); }; const handleInputChange: ChangeEventHandler = (evt) => { @@ -275,242 +274,227 @@ export function RoomSearchModal({ requestClose, pickRoom }: RoomSearchModalProps }, [listFocus.index]); return ( - - - inputRef.current, - returnFocusOnDeactivate: true, - allowOutsideClick: true, - clickOutsideDeactivates: true, - onDeactivate: requestClose, - escapeDeactivates: (evt: KeyboardEvent) => { - evt.stopPropagation(); - return true; - }, + + {pickRoom && ( + - - {pickRoom && ( - - {pickRoom.title} - - {composerIcon(X)} - - - )} - {pickRoom?.errorMessage ? ( - - - {pickRoom.errorMessage} - - - ) : null} - {pickRoom.title} + + {composerIcon(X)} + + + )} + {pickRoom?.errorMessage ? ( + + + {pickRoom.errorMessage} + + + ) : null} + + + + + {roomsToRender.length === 0 && ( + + + {pickRoom + ? result + ? 'No Match Found' + : pickRoom.eligibleRoomIds.length === 0 + ? 'No rooms to forward to' + : 'No rooms match this filter' + : result + ? 'No Match Found' + : 'No Rooms'} + + + {pickRoom + ? result + ? `No match found for "${result.query}".` + : pickRoom.eligibleRoomIds.length === 0 + ? 'You cannot send messages in any joined room yet.' + : 'Try another search, or use # for group rooms and @ for direct messages.' + : result + ? `No match found for "${result.query}".` + : 'You do not have any Rooms to display yet.'} + + + )} + {roomsToRender.length > 0 && ( + +
- - - - {roomsToRender.length === 0 && ( - - - {pickRoom - ? result - ? 'No Match Found' - : pickRoom.eligibleRoomIds.length === 0 - ? 'No rooms to forward to' - : 'No rooms match this filter' - : result - ? 'No Match Found' - : 'No Rooms'} - - - {pickRoom - ? result - ? `No match found for "${result.query}".` - : pickRoom.eligibleRoomIds.length === 0 - ? 'You cannot send messages in any joined room yet.' - : 'Try another search, or use # for group rooms and @ for direct messages.' - : result - ? `No match found for "${result.query}".` - : 'You do not have any Rooms to display yet.'} - - - )} - {roomsToRender.length > 0 && ( - -
- {roomsToRender.map((roomId, index) => { - const room = getRoom(roomId); - if (!room) return null; - - const dm = mDirects.has(roomId); - const dmUserId = dm && getDmUserId(roomId, getRoom, mx.getSafeUserId()); - const dmUsername = dmUserId && getMxIdLocalPart(dmUserId); - const dmUserServer = dmUserId && getMxIdServer(dmUserId); - - const allParents = getAllParents(roomToParents, roomId); - const orphanParents = - allParents && orphanSpaces.filter((o) => allParents.has(o)); - const perfectOrphanParent = - orphanParents && guessPerfectParent(mx, roomId, orphanParents); - - const exactParents = roomToParents.get(roomId); - const perfectParent = - exactParents && guessPerfectParent(mx, roomId, Array.from(exactParents)); - - const unread = roomToUnread.get(roomId); - - return ( - - {dmUserServer && ( - - {dmUserServer} - - )} - {!dm && perfectOrphanParent && ( - - {getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent} - - )} - {unread && ( - - 0} - count={unread.highlight > 0 ? unread.highlight : unread.total} - /> - - )} - - } - before={ - - {dm || room.isSpaceRoom() ? ( - ( - - {nameInitials(room.name)} - - )} - /> - ) : ( - - )} - - } - > - - - {queryHighlighRegex - ? highlightText(queryHighlighRegex, [room.name]) - : room.name} - - {dmUsername && ( - - @ - {queryHighlighRegex - ? highlightText(queryHighlighRegex, [dmUsername]) - : dmUsername} + {roomsToRender.map((roomId, index) => { + const room = getRoom(roomId); + if (!room) return null; + + const dm = mDirects.has(roomId); + const dmUserId = dm && getDmUserId(roomId, getRoom, mx.getSafeUserId()); + const dmUsername = dmUserId && getMxIdLocalPart(dmUserId); + const dmUserServer = dmUserId && getMxIdServer(dmUserId); + + const allParents = getAllParents(roomToParents, roomId); + const orphanParents = allParents && orphanSpaces.filter((o) => allParents.has(o)); + const perfectOrphanParent = + orphanParents && guessPerfectParent(mx, roomId, orphanParents); + + const exactParents = roomToParents.get(roomId); + const perfectParent = + exactParents && guessPerfectParent(mx, roomId, Array.from(exactParents)); + + const unread = roomToUnread.get(roomId); + + return ( + + {dmUserServer && ( + + {dmUserServer} + + )} + {!dm && perfectOrphanParent && ( + + {getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent} + + )} + {unread && ( + + 0} + count={unread.highlight > 0 ? unread.highlight : unread.total} + /> + + )} + + } + before={ + + {dm || room.isSpaceRoom() ? ( + ( + + {nameInitials(room.name)} )} - {!dm && perfectParent && perfectParent !== perfectOrphanParent && ( - - — {getRoom(perfectParent)?.name ?? perfectParent} - - )} - - - ); - })} -
-
- )} -
- - - - {pickRoom ? ( - <> - Type # for rooms and @ for direct messages. Choose a room to - forward this message. - - ) : ( - <> - Type # for rooms, @ for DMs and * for spaces. Hotkey:{' '} - {isMacOS() ? KeySymbol.Command : 'Ctrl'} + k - {' / '} - {isMacOS() ? KeySymbol.Command : 'Ctrl'} + f - - )} - - - - - - + /> + ) : ( + + )} + + } + > + + + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [room.name]) + : room.name} + + {dmUsername && ( + + @ + {queryHighlighRegex + ? highlightText(queryHighlighRegex, [dmUsername]) + : dmUsername} + + )} + {!dm && perfectParent && perfectParent !== perfectOrphanParent && ( + + — {getRoom(perfectParent)?.name ?? perfectParent} + + )} + + + ); + })} +
+
+ )} +
+ + + + {pickRoom ? ( + <> + Type # for rooms and @ for direct messages. Choose a room to forward + this message. + + ) : ( + <> + Type # for rooms, @ for DMs and * for spaces. Hotkey:{' '} + {isMacOS() ? KeySymbol.Command : 'Ctrl'} + k + {' / '} + {isMacOS() ? KeySymbol.Command : 'Ctrl'} + f + + )} + + +
); } @@ -523,25 +507,42 @@ export function SearchModalRenderer() { (event) => { if (isKeyHotkey('mod+k', event)) { event.preventDefault(); - if (opened) { - setOpen(false); - return; - } - - const portalContainer = document.getElementById('portalContainer'); - if (portalContainer && portalContainer.children.length > 0) { - return; - } - setOpen(true); + setOpen(!opened); + return; } }, [opened, setOpen] ) ); - return opened && setOpen(false)} />; + return opened && setOpen(false)} />; +} + +export function SearchWrapper({ requestClose, pickRoom }: RoomSearchModalProps) { + return ( + + + { + evt.stopPropagation(); + return true; + }, + }} + > + + + + + + + ); } export function Search(props: { requestClose: () => void }) { - return ; + return ; } diff --git a/src/app/features/navigate/index.ts b/src/app/features/navigate/index.ts new file mode 100644 index 0000000000..d72d0d0571 --- /dev/null +++ b/src/app/features/navigate/index.ts @@ -0,0 +1 @@ +export * from './NavigateModal'; diff --git a/src/app/features/search/index.ts b/src/app/features/search/index.ts deleted file mode 100644 index addd53308b..0000000000 --- a/src/app/features/search/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Search'; diff --git a/src/app/hooks/router/useNavigateSelected.ts b/src/app/hooks/router/useNavigateSelected.ts new file mode 100644 index 0000000000..81e2168b9c --- /dev/null +++ b/src/app/hooks/router/useNavigateSelected.ts @@ -0,0 +1,12 @@ +import { useMatch } from 'react-router-dom'; +import { getNavigatePath } from '$pages/pathUtils'; + +export const useNavigateSelected = (): boolean => { + const match = useMatch({ + path: getNavigatePath(), + caseSensitive: true, + end: false, + }); + + return !!match; +}; diff --git a/src/app/hooks/router/useProfileSelected.ts b/src/app/hooks/router/useProfileSelected.ts new file mode 100644 index 0000000000..9a7abdde21 --- /dev/null +++ b/src/app/hooks/router/useProfileSelected.ts @@ -0,0 +1,12 @@ +import { useMatch } from 'react-router-dom'; +import { getProfilePath } from '$pages/pathUtils'; + +export const useProfileSelected = (): boolean => { + const match = useMatch({ + path: getProfilePath(), + caseSensitive: true, + end: false, + }); + + return !!match; +}; diff --git a/src/app/pages/MobileFriendly.tsx b/src/app/pages/MobileFriendly.tsx index 83009cda5b..b791bf9066 100644 --- a/src/app/pages/MobileFriendly.tsx +++ b/src/app/pages/MobileFriendly.tsx @@ -1,22 +1,34 @@ import type { ReactNode } from 'react'; import { useMatch } from 'react-router-dom'; import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; -import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from './paths'; +import { + DIRECT_PATH, + EXPLORE_PATH, + HOME_PATH, + INBOX_PATH, + NAVIGATE_PATH, + PROFILE_PATH, + SPACE_PATH, +} from './paths'; type MobileFriendlyClientNavProps = { children: ReactNode; }; -export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavProps) { +export function MobileFriendlySidebarNav({ children }: MobileFriendlyClientNavProps) { const screenSize = useScreenSizeContext(); const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true }); const directMatch = useMatch({ path: DIRECT_PATH, caseSensitive: true, end: true }); const spaceMatch = useMatch({ path: SPACE_PATH, caseSensitive: true, end: true }); const exploreMatch = useMatch({ path: EXPLORE_PATH, caseSensitive: true, end: true }); const inboxMatch = useMatch({ path: INBOX_PATH, caseSensitive: true, end: true }); - + const profileMatch = useMatch({ path: PROFILE_PATH, caseSensitive: true, end: true }); + const navigateMatch = useMatch({ path: NAVIGATE_PATH, caseSensitive: true, end: true }); if ( screenSize === ScreenSize.Mobile && - !(homeMatch || directMatch || spaceMatch || exploreMatch || inboxMatch) + (!(homeMatch || directMatch || spaceMatch || exploreMatch) || + profileMatch || + inboxMatch || + navigateMatch) ) { return null; } @@ -24,6 +36,22 @@ export function MobileFriendlyClientNav({ children }: MobileFriendlyClientNavPro return children; } +export function MobileFriendlyBottomNav({ children }: MobileFriendlyClientNavProps) { + const screenSize = useScreenSizeContext(); + const homeMatch = useMatch({ path: HOME_PATH, caseSensitive: true, end: true }); + const directMatch = useMatch({ path: DIRECT_PATH, caseSensitive: true, end: true }); + const spaceMatch = useMatch({ path: SPACE_PATH, caseSensitive: true, end: true }); + const settingsMatch = useMatch({ path: '/settings/', caseSensitive: true, end: true }); + if ( + screenSize !== ScreenSize.Mobile || + (!homeMatch && !directMatch && !spaceMatch) || + settingsMatch + ) { + return null; + } + + return children; +} type MobileFriendlyPageNavProps = { path: string; children: ReactNode; diff --git a/src/app/pages/Router.tsx b/src/app/pages/Router.tsx index 56c24a899c..1605b24ecc 100644 --- a/src/app/pages/Router.tsx +++ b/src/app/pages/Router.tsx @@ -28,7 +28,7 @@ import type { Sessions } from '$state/sessions'; import { getFallbackSession, MATRIX_SESSIONS_KEY } from '$state/sessions'; import { getLocalStorageItem } from '$state/utils/atomWithLocalStorage'; import { NotificationJumper } from '$hooks/useNotificationJumper'; -import { SearchModalRenderer } from '$features/search'; +import { SearchModalRenderer } from '$features/navigate'; import { GlobalKeyboardShortcuts } from '$components/GlobalKeyboardShortcuts'; import { CallEmbedProvider } from '$components/CallEmbedProvider'; import { AuthLayout, Login, Register, ResetPassword } from './auth'; @@ -53,6 +53,8 @@ import { CREATE_PATH, TO_ROOM_EVENT_PATH, SETTINGS_PATH, + NAVIGATE_PATH, + PROFILE_PATH, } from './paths'; import { getAppPathFromHref, @@ -73,7 +75,11 @@ import { Notifications, Inbox, Invites } from './client/inbox'; import { setAfterLoginRedirectPath } from './afterLoginRedirectPath'; import { WelcomePage } from './client/WelcomePage'; import { SidebarNav } from './client/SidebarNav'; -import { MobileFriendlyPageNav, MobileFriendlyClientNav } from './MobileFriendly'; +import { + MobileFriendlyPageNav, + MobileFriendlySidebarNav, + MobileFriendlyBottomNav, +} from './MobileFriendly'; import { ClientInitStorageAtom } from './client/ClientInitStorageAtom'; import { AuthRouteThemeManager, UnAuthRouteThemeManager } from './ThemeManager'; import { ClientRoomsNotificationPreferences } from './client/ClientRoomsNotificationPreferences'; @@ -81,6 +87,9 @@ import { HomeCreateRoom } from './client/home/CreateRoom'; import { Create } from './client/create'; import { ToRoomEvent } from './client/ToRoomEvent'; import { CallStatusRenderer } from './CallStatusRenderer'; +import { UserQuickToolsProvider } from '$components/UserQuickToolsProvider'; +import { Navigate } from './client/navigate'; +import { ProfileMobile } from './client/profile'; /** * Returns true if there is at least one stored session. @@ -182,15 +191,18 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) + - + } > + + + @@ -346,6 +358,8 @@ export const createRouter = (clientConfig: ClientConfig, screenSize: ScreenSize) } /> } /> + } /> + } /> } /> } sticky={ - - - {oldSidebar ? ( - <> - - -
- {/*PROBS ADD SETTINGSTAB HERE WHEN ADDING THE STATUSES*/} - -
- - ) : ( - <> - {isCollapsed && ( + <> + {(oldSidebar || isCollapsed) && ( + + + {oldSidebar ? ( + <> + + + + ) : ( <> - + )} - - - - - + + )} + {!compact && ( +
+ +
)} -
+ } /> - {!oldSidebar && } ); } diff --git a/src/app/pages/client/create/Create.tsx b/src/app/pages/client/create/Create.tsx index d22ba50c87..c9a77e7f9c 100644 --- a/src/app/pages/client/create/Create.tsx +++ b/src/app/pages/client/create/Create.tsx @@ -18,6 +18,7 @@ import { settingsAtom } from '$state/settings'; import { SidebarResizer } from '../sidebar/SidebarResizer'; import { useSetAtom } from 'jotai'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { UserQuickTools } from '../sidebar/UserQuickTools'; export function Create() { const { navigateSpace } = useRoomNavigate(); @@ -32,6 +33,7 @@ export function Create() { const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const hideText = curWidth <= 80 && !isMobile; + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); return ( <> @@ -71,6 +73,7 @@ export function Create() { setAnnouncement={setIsResizingSidebar} /> + {!oldSidebar && !isMobile && }
)} diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx index c9a3bffdfe..bc3bf0ed37 100644 --- a/src/app/pages/client/direct/Direct.tsx +++ b/src/app/pages/client/direct/Direct.tsx @@ -65,6 +65,7 @@ import { useDirectRooms } from './useDirectRooms'; import { SidebarResizer } from '$pages/client/sidebar/SidebarResizer'; import { useScreenSizeContext, ScreenSize } from '$hooks/useScreenSize'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { UserQuickTools } from '../sidebar/UserQuickTools'; type DirectMenuProps = { requestClose: () => void; @@ -270,6 +271,7 @@ export function Direct() { const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const hideText = curWidth <= 80 && !isMobile; + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); return ( )} + {!oldSidebar && !isMobile && } ); } diff --git a/src/app/pages/client/explore/Explore.tsx b/src/app/pages/client/explore/Explore.tsx index 60796783a9..c78aec24f9 100644 --- a/src/app/pages/client/explore/Explore.tsx +++ b/src/app/pages/client/explore/Explore.tsx @@ -54,6 +54,7 @@ import { isServerName } from '$utils/matrix'; import { useScreenSizeContext, ScreenSize } from '$hooks/useScreenSize'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; import { useSetAtom } from 'jotai'; +import { UserQuickTools } from '../sidebar/UserQuickTools'; type AddServerProps = { hideText?: boolean; @@ -267,6 +268,7 @@ export function Explore() { const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const hideText = curWidth <= 80 && !isMobile; + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); return ( )} + {!oldSidebar && !isMobile && } ); } diff --git a/src/app/pages/client/home/Home.tsx b/src/app/pages/client/home/Home.tsx index 3b2da2945c..c32f6766d2 100644 --- a/src/app/pages/client/home/Home.tsx +++ b/src/app/pages/client/home/Home.tsx @@ -81,6 +81,7 @@ import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { useClientConfig } from '$hooks/useClientConfig'; import { getMxIdServer } from '$utils/mxIdHelper'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { UserQuickTools } from '../sidebar/UserQuickTools'; type HomeMenuProps = { requestClose: () => void; @@ -319,6 +320,7 @@ export function Home() { const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const hideText = curWidth <= 80 && !isMobile; + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); return ( )} + {!oldSidebar && !isMobile && } ); } diff --git a/src/app/pages/client/inbox/Inbox.tsx b/src/app/pages/client/inbox/Inbox.tsx index 7dca68e169..e0d809edad 100644 --- a/src/app/pages/client/inbox/Inbox.tsx +++ b/src/app/pages/client/inbox/Inbox.tsx @@ -14,6 +14,7 @@ import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { useInviteCount } from '$hooks/useInviteCount'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; import { useSetAtom } from 'jotai'; +import { UserQuickTools } from '../sidebar/UserQuickTools'; function InvitesNavItem({ hideText }: { hideText?: boolean }) { const invitesSelected = useInboxInvitesSelected(); @@ -58,6 +59,7 @@ export function Inbox() { const setIsResizingSidebar = useSetAtom(isResizingSidebarAtom); const [roomSidebarWidth, setRoomSidebarWidth] = useSetting(settingsAtom, 'roomSidebarWidth'); const [curWidth, setCurWidth] = useState(roomSidebarWidth); + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); useEffect(() => { setCurWidth(roomSidebarWidth); @@ -131,6 +133,7 @@ export function Inbox() { setAnnouncement={setIsResizingSidebar} /> )} + {!oldSidebar && !isMobile && } ); } diff --git a/src/app/pages/client/navigate/Navigate.tsx b/src/app/pages/client/navigate/Navigate.tsx new file mode 100644 index 0000000000..1e63ee51a2 --- /dev/null +++ b/src/app/pages/client/navigate/Navigate.tsx @@ -0,0 +1,91 @@ +import { Box, Scroll, toRem, Text, color, config } from 'folds'; +import { SquaresFour, sizedIcon } from '$components/icons/phosphor'; +import { + Page, + PageContent, + PageContentCenter, + PageHeroSection, + PageNav, + PageNavHeader, +} from '$components/page'; +import { useEffect, useState } from 'react'; +import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { SidebarResizer } from '../sidebar/SidebarResizer'; +import { useSetAtom } from 'jotai'; +import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { ListMagnifyingGlassIcon } from '@phosphor-icons/react'; +import { RoomSearchModal } from '$features/navigate'; + +export function Navigate() { + const setIsResizingSidebar = useSetAtom(isResizingSidebarAtom); + const [roomSidebarWidth, setRoomSidebarWidth] = useSetting(settingsAtom, 'roomSidebarWidth'); + const [curWidth, setCurWidth] = useState(roomSidebarWidth); + + useEffect(() => { + setCurWidth(roomSidebarWidth); + }, [roomSidebarWidth]); + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + const hideText = curWidth <= 80 && !isMobile; + + return ( + <> + {!isMobile && ( + + + + + {!hideText ? ( + + + Navigate + + + ) : ( + sizedIcon(SquaresFour, '200', { filled: true }) + )} + + + + + + )} + + + + + + + + {sizedIcon(ListMagnifyingGlassIcon, '600')} + + + + + + + + + + ); +} diff --git a/src/app/pages/client/navigate/index.ts b/src/app/pages/client/navigate/index.ts new file mode 100644 index 0000000000..7571cfdeca --- /dev/null +++ b/src/app/pages/client/navigate/index.ts @@ -0,0 +1 @@ +export * from './Navigate'; diff --git a/src/app/pages/client/profile/Profile.tsx b/src/app/pages/client/profile/Profile.tsx new file mode 100644 index 0000000000..11f7b8a499 --- /dev/null +++ b/src/app/pages/client/profile/Profile.tsx @@ -0,0 +1,133 @@ +import { Box, Scroll, toRem, Text, color, config, Menu, Icon, Icons, Line, MenuItem } from 'folds'; +import { SquaresFour, sizedIcon } from '$components/icons/phosphor'; +import { Page, PageHeroSection, PageNav, PageNavHeader } from '$components/page'; +import { useEffect, useState } from 'react'; +import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; +import { useSetting } from '$state/hooks/settings'; +import { settingsAtom } from '$state/settings'; +import { SidebarResizer } from '../sidebar/SidebarResizer'; +import { useSetAtom } from 'jotai'; +import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { AccountMenuOption, PresenceMenuOption } from '../sidebar/UserMenuTab'; +import { useMatrixClient } from '$hooks/useMatrixClient'; +import { GlobalUserHeroName, UserHero } from '$components/user-profile/UserHero'; +import { getMxIdLocalPart, mxcUrlToHttp } from '$utils/matrix'; +import { useMediaAuthentication } from '$hooks/useMediaAuthentication'; +import { useUserPresence } from '$hooks/useUserPresence'; +import { useUserProfile } from '$hooks/useUserProfile'; +import { useOpenSettings } from '$features/settings'; + +export function ProfileMobile() { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const openSettings = useOpenSettings(); + + const userId = mx.getUserId() ?? ''; + const profile = useUserProfile(userId); + const presence = useUserPresence(userId); + + const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId; + const heroAvatarUrl = profile.avatarUrl + ? (mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 160, 160, 'crop') ?? undefined) + : undefined; + + const parsedBanner = + typeof profile.bannerUrl === 'string' ? profile.bannerUrl.replace(/^"|"$/g, '') : undefined; + const heroBannerUrl = parsedBanner + ? (mxcUrlToHttp(mx, parsedBanner, useAuthentication, 640, 192, 'scale') ?? undefined) + : undefined; + + const setIsResizingSidebar = useSetAtom(isResizingSidebarAtom); + const [roomSidebarWidth, setRoomSidebarWidth] = useSetting(settingsAtom, 'roomSidebarWidth'); + const [curWidth, setCurWidth] = useState(roomSidebarWidth); + + useEffect(() => { + setCurWidth(roomSidebarWidth); + }, [roomSidebarWidth]); + const screenSize = useScreenSizeContext(); + const isMobile = screenSize === ScreenSize.Mobile; + const hideText = curWidth <= 80 && !isMobile; + + return ( + <> + {!isMobile && ( + + + + + {!hideText ? ( + + + Profile + + + ) : ( + sizedIcon(SquaresFour, '200', { filled: true }) + )} + + + + + + )} + + + + + + + + + + + + + + + + + + } + onClick={() => openSettings()} + > + + Settings + + + + + + + + + + ); +} diff --git a/src/app/pages/client/profile/index.ts b/src/app/pages/client/profile/index.ts new file mode 100644 index 0000000000..963df00a60 --- /dev/null +++ b/src/app/pages/client/profile/index.ts @@ -0,0 +1 @@ +export * from './Profile'; diff --git a/src/app/pages/client/sidebar/InboxTab.tsx b/src/app/pages/client/sidebar/InboxTab.tsx index fe3f636e93..8141fb7593 100644 --- a/src/app/pages/client/sidebar/InboxTab.tsx +++ b/src/app/pages/client/sidebar/InboxTab.tsx @@ -17,13 +17,17 @@ import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { useNavToActivePathAtom } from '$state/hooks/navToActivePath'; import { useInviteCount } from '$hooks/useInviteCount'; import { getPhosphorIconSize, Tray } from '$components/icons/phosphor'; +import { Text, Box, color } from 'folds'; +import { searchModalAtom } from '$state/searchModal'; -export function InboxTab({ isBottom }: { isBottom?: boolean }) { +export function InboxTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) { const screenSize = useScreenSizeContext(); const navigate = useNavigate(); const navToActivePath = useAtomValue(useNavToActivePathAtom()); const inboxSelected = useInboxSelected(); const inviteCount = useInviteCount(); + const isSearch = useAtomValue(searchModalAtom); + const opened = inboxSelected && !isSearch; const handleInboxClick = () => { if (screenSize === ScreenSize.Mobile) { @@ -41,21 +45,29 @@ export function InboxTab({ isBottom }: { isBottom?: boolean }) { }; return ( - + {(triggerRef) => ( - - - + + + + + {isMobile && ( + + Inbox + + )} + )} {inviteCount > 0 && } diff --git a/src/app/pages/client/sidebar/MessageTab.tsx b/src/app/pages/client/sidebar/MessageTab.tsx new file mode 100644 index 0000000000..212d8b48ab --- /dev/null +++ b/src/app/pages/client/sidebar/MessageTab.tsx @@ -0,0 +1,57 @@ +import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '$components/sidebar'; +import { getPhosphorIconSize } from '$components/icons/phosphor'; +import { matchPath, useNavigate } from 'react-router-dom'; +import { HOME_PATH, SETTINGS_PATH } from '$pages/paths'; +import { ChatTextIcon } from '@phosphor-icons/react'; +import { useAtom } from 'jotai'; +import { searchModalAtom } from '$state/searchModal'; +import { useInboxSelected } from '$hooks/router/useInbox'; +import { Box, color, Text } from 'folds'; +import { useNavigateSelected } from '$hooks/router/useNavigateSelected'; +import { useProfileSelected } from '$hooks/router/useProfileSelected'; + +export function MessageTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) { + const navigate = useNavigate(); + const [searchSelected] = useAtom(searchModalAtom); + const navigateRouteActive = useNavigateSelected(); + const profileRouteActive = useProfileSelected(); + const inboxSelected = useInboxSelected(); + const opened = !( + matchPath(SETTINGS_PATH, location.pathname) || + searchSelected || + navigateRouteActive || + profileRouteActive || + inboxSelected + ); + const openSettings = () => navigate(HOME_PATH); + + return ( + + + {(triggerRef) => ( + + + + + {isMobile && ( + + Messages + + )} + + )} + + + ); +} diff --git a/src/app/pages/client/sidebar/NavigateTab.tsx b/src/app/pages/client/sidebar/NavigateTab.tsx new file mode 100644 index 0000000000..e6e717958b --- /dev/null +++ b/src/app/pages/client/sidebar/NavigateTab.tsx @@ -0,0 +1,49 @@ +import { useAtom } from 'jotai'; +import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '$components/sidebar'; +import { searchModalAtom } from '$state/searchModal'; +import { ListMagnifyingGlassIcon } from '@phosphor-icons/react'; +import { getPhosphorIconSize } from '$components/icons/phosphor'; +import { Text, Box, color } from 'folds'; +import { useNavigate } from 'react-router-dom'; +import { getNavigatePath } from '$pages/pathUtils'; +import { useNavigateSelected } from '$hooks/router/useNavigateSelected'; + +export function NavigateTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) { + const [opened, setOpen] = useAtom(searchModalAtom); + const navigateRouteActive = useNavigateSelected(); + const isNavigate = opened || navigateRouteActive; + const navigate = useNavigate(); + const open = () => { + if (isMobile) navigate(getNavigatePath()); + else setOpen(true); + }; + + return ( + + + {(triggerRef) => ( + + + + + {isMobile && ( + + Navigate + + )} + + )} + + + ); +} diff --git a/src/app/pages/client/sidebar/SearchTab.tsx b/src/app/pages/client/sidebar/SearchTab.tsx deleted file mode 100644 index 0a8045860d..0000000000 --- a/src/app/pages/client/sidebar/SearchTab.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useAtom } from 'jotai'; -import { SidebarAvatar, SidebarItem, SidebarItemTooltip } from '$components/sidebar'; -import { searchModalAtom } from '$state/searchModal'; -import { ListMagnifyingGlassIcon } from '@phosphor-icons/react'; -import { getPhosphorIconSize } from '$components/icons/phosphor'; - -export function SearchTab({ isBottom }: { isBottom?: boolean }) { - const [opened, setOpen] = useAtom(searchModalAtom); - - const open = () => setOpen(true); - - return ( - - - {(triggerRef) => ( - - - - )} - - - ); -} diff --git a/src/app/pages/client/sidebar/SettingsTab.tsx b/src/app/pages/client/sidebar/SettingsTab.tsx index 37372e2ebe..7ca47d18aa 100644 --- a/src/app/pages/client/sidebar/SettingsTab.tsx +++ b/src/app/pages/client/sidebar/SettingsTab.tsx @@ -3,8 +3,9 @@ import { GearSix, getPhosphorIconSize } from '$components/icons/phosphor'; import { useOpenSettings } from '$features/settings'; import { matchPath } from 'react-router-dom'; import { SETTINGS_PATH } from '$pages/paths'; +import { color } from 'folds'; -export function SettingsTab({ isBottom }: { isBottom?: boolean }) { +export function SettingsTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) { const opened = !!matchPath(SETTINGS_PATH, location.pathname); const openSettings = useOpenSettings(); @@ -16,6 +17,7 @@ export function SettingsTab({ isBottom }: { isBottom?: boolean }) { )} diff --git a/src/app/pages/client/sidebar/UserMenuTab.tsx b/src/app/pages/client/sidebar/UserMenuTab.tsx index efbb30283c..4b656b9c76 100644 --- a/src/app/pages/client/sidebar/UserMenuTab.tsx +++ b/src/app/pages/client/sidebar/UserMenuTab.tsx @@ -39,7 +39,7 @@ import { UnreadBadge, UnreadBadgeCenter } from '$components/unread-badge'; import { Check, chipIcon, Plus } from '$components/icons/phosphor'; import { useSessionProfiles } from '$hooks/useSessionProfiles'; import { useClientConfig } from '$hooks/useClientConfig'; -import { getHomePath, getLoginPath, withSearchParam } from '$pages/pathUtils'; +import { getHomePath, getLoginPath, getProfilePath, withSearchParam } from '$pages/pathUtils'; import { initClient, logoutClient, stopClient } from '$client/initMatrix'; import { useAtom, useAtomValue, useSetAtom } from 'jotai'; import { useNavigate } from 'react-router-dom'; @@ -48,6 +48,7 @@ import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { setUserPresence } from '$utils/presence'; import { useSetting } from '$state/hooks/settings'; import { settingsAtom } from '$state/settings'; +import { useProfileSelected } from '$hooks/router/useProfileSelected'; const log = createLogger('AccountSwitcherTab'); @@ -377,7 +378,7 @@ const PresenceOptions: Array<{ value: Presence; label: string }> = [ { value: Presence.Offline, label: 'Offline' }, ]; -export function PresenceMenuOption() { +export function PresenceMenuOption({ initialOpen }: { initialOpen: boolean }) { const mx = useMatrixClient(); const [sendPresence] = useSetting(settingsAtom, 'sendPresence'); @@ -387,7 +388,7 @@ export function PresenceMenuOption() { const isMobile = screenSize === ScreenSize.Mobile; const currentPresence = presence?.presence ?? Presence.Online; - const [isOpen, setIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(initialOpen); const { hoverProps } = useHover({ onHoverChange: (h) => { if (!isMobile) setIsOpen(h); @@ -525,9 +526,11 @@ export function PresenceMenuOption() { ); } -export function UserMenuTab({ isBottom }: { isBottom?: boolean }) { +export function UserMenuTab({ isBottom, isMobile }: { isBottom?: boolean; isMobile?: boolean }) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); + const profileSelected = useProfileSelected(); + const navigate = useNavigate(); const userId = mx.getUserId() ?? ''; const profile = useUserProfile(userId); @@ -552,7 +555,12 @@ export function UserMenuTab({ isBottom }: { isBottom?: boolean }) { ? (mxcUrlToHttp(mx, parsedBanner, useAuthentication, 640, 192, 'scale') ?? undefined) : undefined; - const handleToggle: MouseEventHandler = (evt) => { + const handleToggle: MouseEventHandler = (evt) => { + if (isMobile) { + navigate(getProfilePath()); + return; + } + const cords = evt.currentTarget.getBoundingClientRect(); setMenuAnchor((cur) => (cur ? undefined : cords)); }; @@ -560,22 +568,39 @@ export function UserMenuTab({ isBottom }: { isBottom?: boolean }) { const handleCloseMenu = () => setMenuAnchor(undefined); return ( - - + + {(triggerRef) => ( - } - > - - {nameInitials(displayName)}} - /> + + + } + > + + {nameInitials(displayName)}} + /> + + - + {isMobile && ( + + Account + + )} + )} @@ -626,7 +651,7 @@ export function UserMenuTab({ isBottom }: { isBottom?: boolean }) { - + diff --git a/src/app/pages/client/sidebar/UserQuickTools.css.ts b/src/app/pages/client/sidebar/UserQuickTools.css.ts index 8cd7330381..0e194fd8d1 100644 --- a/src/app/pages/client/sidebar/UserQuickTools.css.ts +++ b/src/app/pages/client/sidebar/UserQuickTools.css.ts @@ -2,13 +2,11 @@ import { style } from '@vanilla-extract/css'; import { color, config, toRem } from 'folds'; export const UserQuickTools = style({ - backgroundColor: color.SurfaceVariant.Container, - color: color.SurfaceVariant.OnContainer, - position: 'absolute', + backgroundColor: color.Surface.Container, + color: color.Surface.OnContainer, zIndex: '1000', height: toRem(74), bottom: '0', - left: toRem(-66), padding: config.space.S300, borderTop: `${config.borderWidth.B300} solid ${color.Background.ContainerLine}`, }); diff --git a/src/app/pages/client/sidebar/UserQuickTools.tsx b/src/app/pages/client/sidebar/UserQuickTools.tsx index a6f96dcfc0..e65799d612 100644 --- a/src/app/pages/client/sidebar/UserQuickTools.tsx +++ b/src/app/pages/client/sidebar/UserQuickTools.tsx @@ -1,25 +1,21 @@ import { Box, config, toRem } from 'folds'; import { InboxTab } from './InboxTab'; -import { SearchTab } from './SearchTab'; +import { NavigateTab } from './NavigateTab'; import { SettingsTab } from './SettingsTab'; -import { useAtom } from 'jotai'; -import { isResizingSidebarAtom } from '$state/isResizingSidebar'; import * as css from './UserQuickTools.css'; -import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize'; import { UserMenuTab } from './UserMenuTab'; +import { MessageTab } from './MessageTab'; export function UserQuickTools({ width, + compact, }: { isCollapsed?: boolean; underOutstep?: boolean; - width: number; + width?: number; + compact: boolean; }) { - const screenSize = useScreenSizeContext(); - const compact = screenSize === ScreenSize.Mobile; - - const [isResizingSidebar] = useAtom(isResizingSidebarAtom); - const isCollapsed = compact ? false : width < 190 + 66; + const isCollapsed = compact ? false : (width ?? 0) < 190 + 66; return ( <> @@ -28,30 +24,48 @@ export function UserQuickTools({
- - - {!isCollapsed && ( - <> - - - - - )} - + {compact ? ( + <> + + + + + + ) : ( + <> + + + {!isCollapsed && ( + <> + + + + + )} + + + )}
)} diff --git a/src/app/pages/client/space/Space.tsx b/src/app/pages/client/space/Space.tsx index 60aefa740d..1a6fd24aba 100644 --- a/src/app/pages/client/space/Space.tsx +++ b/src/app/pages/client/space/Space.tsx @@ -109,6 +109,7 @@ import { ModalWide } from '$styles/Modal.css'; import { ImageViewer } from '$components/image-viewer'; import * as css from './styles.css'; import { isResizingSidebarAtom } from '$state/isResizingSidebar'; +import { UserQuickTools } from '../sidebar/UserQuickTools'; const debugLog = createDebugLogger('Space'); @@ -859,6 +860,8 @@ export function Space() { const screenSize = useScreenSizeContext(); const isMobile = screenSize === ScreenSize.Mobile; const hideText = curWidth <= 80 && !isMobile; + const [oldSidebar] = useSetting(settingsAtom, 'oldSidebar'); + return ( )} + {!oldSidebar && !isMobile && } ); } diff --git a/src/app/pages/pathUtils.ts b/src/app/pages/pathUtils.ts index 4a95f47fcd..bf17af8f95 100644 --- a/src/app/pages/pathUtils.ts +++ b/src/app/pages/pathUtils.ts @@ -28,6 +28,8 @@ import { SPACE_ROOM_PATH, SPACE_SEARCH_PATH, CREATE_PATH, + NAVIGATE_PATH, + PROFILE_PATH, } from './paths'; export const joinPathComponent = (path: Path): string => path.pathname + path.search + path.hash; @@ -154,6 +156,8 @@ export const getExploreServerPath = (server: string): string => { }; export const getCreatePath = (): string => CREATE_PATH; +export const getNavigatePath = (): string => NAVIGATE_PATH; +export const getProfilePath = (): string => PROFILE_PATH; export const getInboxPath = (): string => INBOX_PATH; export const getInboxNotificationsPath = (): string => INBOX_NOTIFICATIONS_PATH; diff --git a/src/app/pages/paths.ts b/src/app/pages/paths.ts index 1ac57b7568..f465dd25ef 100644 --- a/src/app/pages/paths.ts +++ b/src/app/pages/paths.ts @@ -79,6 +79,8 @@ export type ExploreServerPathSearchParams = { export const EXPLORE_SERVER_PATH = `/explore/${SERVER_PATH_SEGMENT}`; export const CREATE_PATH = '/create'; +export const NAVIGATE_PATH = '/navigate'; +export const PROFILE_PATH = '/profile/'; export const NOTIFICATIONS_PATH_SEGMENT = 'notifications/'; export const INVITES_PATH_SEGMENT = 'invites/'; diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 22445a35b5..d1294b49d8 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -344,7 +344,7 @@ export const defaultSettings: Settings = { saveStickerEmojiBandwidth: false, subspaceHierarchyLimit: 3, alwaysShowCallButton: false, - joinCallOnSingleClick: true, + joinCallOnSingleClick: false, faviconForMentionsOnly: false, highlightMentions: true, pkCompat: false,