diff --git a/src/app/components/BackRouteHandler.tsx b/src/app/components/BackRouteHandler.tsx new file mode 100644 index 00000000..fa3d7592 --- /dev/null +++ b/src/app/components/BackRouteHandler.tsx @@ -0,0 +1,86 @@ +import { ReactNode, useCallback } from 'react'; +import { matchPath, useLocation, useNavigate } from 'react-router-dom'; +import { + getDirectPath, + getExplorePath, + getHomePath, + getInboxPath, + getSpacePath, +} from '../pages/pathUtils'; +import { DIRECT_PATH, EXPLORE_PATH, HOME_PATH, INBOX_PATH, SPACE_PATH } from '../pages/paths'; + +type BackRouteHandlerProps = { + children: (onBack: () => void) => ReactNode; +}; +export function BackRouteHandler({ children }: BackRouteHandlerProps) { + const navigate = useNavigate(); + const location = useLocation(); + + const goBack = useCallback(() => { + if ( + matchPath( + { + path: HOME_PATH, + caseSensitive: true, + end: false, + }, + location.pathname + ) + ) { + navigate(getHomePath()); + return; + } + if ( + matchPath( + { + path: DIRECT_PATH, + caseSensitive: true, + end: false, + }, + location.pathname + ) + ) { + navigate(getDirectPath()); + return; + } + const spaceMatch = matchPath( + { + path: SPACE_PATH, + caseSensitive: true, + end: false, + }, + location.pathname + ); + if (spaceMatch?.params.spaceIdOrAlias) { + navigate(getSpacePath(spaceMatch.params.spaceIdOrAlias)); + return; + } + if ( + matchPath( + { + path: EXPLORE_PATH, + caseSensitive: true, + end: false, + }, + location.pathname + ) + ) { + navigate(getExplorePath()); + return; + } + if ( + matchPath( + { + path: INBOX_PATH, + caseSensitive: true, + end: false, + }, + location.pathname + ) + ) { + navigate(getInboxPath()); + } + }, [navigate, location]); + + return children(goBack); +} diff --git a/src/app/components/page/Page.tsx b/src/app/components/page/Page.tsx index 4ccb1ec0..a8b9ea04 100644 --- a/src/app/components/page/Page.tsx +++ b/src/app/components/page/Page.tsx @@ -87,15 +87,17 @@ export const Page = as<'div'>(({ className, ...props }, ref) => ( /> )); -export const PageHeader = as<'div'>(({ className, ...props }, ref) => ( - <Header - as="header" - size="600" - className={classNames(css.PageHeader, className)} - {...props} - ref={ref} - /> -)); +export const PageHeader = as<'div', css.PageHeaderVariants>( + ({ className, balance, ...props }, ref) => ( + <Header + as="header" + size="600" + className={classNames(css.PageHeader({ balance }), className)} + {...props} + ref={ref} + /> + ) +); export const PageContent = as<'div'>(({ className, ...props }, ref) => ( <div className={classNames(css.PageContent, className)} {...props} ref={ref} /> diff --git a/src/app/components/page/style.css.ts b/src/app/components/page/style.css.ts index 4807a227..23f2da49 100644 --- a/src/app/components/page/style.css.ts +++ b/src/app/components/page/style.css.ts @@ -1,4 +1,5 @@ import { style } from '@vanilla-extract/css'; +import { recipe, RecipeVariants } from '@vanilla-extract/recipes'; import { DefaultReset, color, config, toRem } from 'folds'; export const PageNav = style({ @@ -33,11 +34,21 @@ export const PageNavContent = style({ paddingBottom: config.space.S700, }); -export const PageHeader = style({ - paddingLeft: config.space.S400, - paddingRight: config.space.S200, - borderBottomWidth: config.borderWidth.B300, +export const PageHeader = recipe({ + base: { + paddingLeft: config.space.S400, + paddingRight: config.space.S200, + borderBottomWidth: config.borderWidth.B300, + }, + variants: { + balance: { + true: { + paddingLeft: config.space.S200, + }, + }, + }, }); +export type PageHeaderVariants = RecipeVariants<typeof PageHeader>; export const PageContent = style([ DefaultReset, diff --git a/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx b/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx index 1cec6599..028cd560 100644 --- a/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx +++ b/src/app/features/join-before-navigate/JoinBeforeNavigate.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Scroll, Text, toRem } from 'folds'; +import { Box, Icon, IconButton, Icons, Scroll, Text, toRem } from 'folds'; import { useAtomValue } from 'jotai'; import { RoomCard } from '../../components/room-card'; import { RoomTopicViewer } from '../../components/room-topic-viewer'; @@ -8,6 +8,8 @@ import { RoomSummaryLoader } from '../../components/RoomSummaryLoader'; import { useRoomNavigate } from '../../hooks/useRoomNavigate'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { allRoomsAtom } from '../../state/room-list/roomList'; +import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../components/BackRouteHandler'; type JoinBeforeNavigateProps = { roomIdOrAlias: string; eventId?: string; viaServers?: string[] }; export function JoinBeforeNavigate({ @@ -18,6 +20,7 @@ export function JoinBeforeNavigate({ const mx = useMatrixClient(); const allRooms = useAtomValue(allRoomsAtom); const { navigateRoom, navigateSpace } = useRoomNavigate(); + const screenSize = useScreenSizeContext(); const handleView = (roomId: string) => { if (mx.getRoom(roomId)?.isSpaceRoom()) { @@ -29,11 +32,24 @@ export function JoinBeforeNavigate({ return ( <Page> - <PageHeader> - <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> - <Text size="H3" truncate> - {roomIdOrAlias} - </Text> + <PageHeader balance> + <Box grow="Yes" gap="200"> + <Box shrink="No"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + )} + </Box> + <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> + <Text size="H3" truncate> + {roomIdOrAlias} + </Text> + </Box> </Box> </PageHeader> <Box grow="Yes"> diff --git a/src/app/features/lobby/LobbyHeader.tsx b/src/app/features/lobby/LobbyHeader.tsx index e01d3ad5..fa415bd2 100644 --- a/src/app/features/lobby/LobbyHeader.tsx +++ b/src/app/features/lobby/LobbyHeader.tsx @@ -31,6 +31,8 @@ import { IPowerLevels, usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { UseStateProvider } from '../../components/UseStateProvider'; import { LeaveSpacePrompt } from '../../components/leave-space-prompt'; import { stopPropagation } from '../../utils/keyboard'; +import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../components/BackRouteHandler'; type LobbyMenuProps = { roomId: string; @@ -123,6 +125,7 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) { const space = useSpace(); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const [menuAnchor, setMenuAnchor] = useState<RectCords>(); + const screenSize = useScreenSizeContext(); const name = useRoomName(space); const avatarMxc = useRoomAvatar(space); @@ -133,42 +136,72 @@ export function LobbyHeader({ showProfile, powerLevels }: LobbyHeaderProps) { }; return ( - <PageHeader className={showProfile ? undefined : css.Header}> + <PageHeader className={showProfile ? undefined : css.Header} balance> <Box grow="Yes" alignItems="Center" gap="200"> - <Box grow="Yes" basis="No" /> - <Box justifyContent="Center" alignItems="Center" gap="300"> - {showProfile && ( - <> - <Avatar size="300"> - <RoomAvatar - roomId={space.roomId} - src={avatarUrl} - alt={name} - renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>} - /> - </Avatar> - <Text size="H3" truncate> - {name} - </Text> - </> + {screenSize === ScreenSize.Mobile ? ( + <> + <Box shrink="No"> + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + </Box> + <Box grow="Yes" justifyContent="Center"> + {showProfile && ( + <Text size="H3" truncate> + {name} + </Text> + )} + </Box> + </> + ) : ( + <> + <Box grow="Yes" basis="No" /> + <Box justifyContent="Center" alignItems="Center" gap="300"> + {showProfile && ( + <> + <Avatar size="300"> + <RoomAvatar + roomId={space.roomId} + src={avatarUrl} + alt={name} + renderFallback={() => <Text size="H4">{nameInitials(name)}</Text>} + /> + </Avatar> + <Text size="H3" truncate> + {name} + </Text> + </> + )} + </Box> + </> + )} + <Box + shrink="No" + grow={screenSize === ScreenSize.Mobile ? 'No' : 'Yes'} + basis={screenSize === ScreenSize.Mobile ? 'Yes' : 'No'} + justifyContent="End" + > + {screenSize !== ScreenSize.Mobile && ( + <TooltipProvider + position="Bottom" + offset={4} + tooltip={ + <Tooltip> + <Text>Members</Text> + </Tooltip> + } + > + {(triggerRef) => ( + <IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}> + <Icon size="400" src={Icons.User} /> + </IconButton> + )} + </TooltipProvider> )} - </Box> - <Box shrink="No" grow="Yes" basis="No" justifyContent="End"> - <TooltipProvider - position="Bottom" - offset={4} - tooltip={ - <Tooltip> - <Text>Members</Text> - </Tooltip> - } - > - {(triggerRef) => ( - <IconButton ref={triggerRef} onClick={() => setPeopleDrawer((drawer) => !drawer)}> - <Icon size="400" src={Icons.User} /> - </IconButton> - )} - </TooltipProvider> <TooltipProvider position="Bottom" align="End" diff --git a/src/app/features/room/RoomViewHeader.tsx b/src/app/features/room/RoomViewHeader.tsx index 709f66c8..0b8ef74e 100644 --- a/src/app/features/room/RoomViewHeader.tsx +++ b/src/app/features/room/RoomViewHeader.tsx @@ -52,6 +52,7 @@ import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize'; import { stopPropagation } from '../../utils/keyboard'; import { getMatrixToRoom } from '../../plugins/matrix-to'; import { getViaServers } from '../../plugins/via-servers'; +import { BackRouteHandler } from '../../components/BackRouteHandler'; type RoomMenuProps = { room: Room; @@ -203,19 +204,36 @@ export function RoomViewHeader() { }; return ( - <PageHeader> + <PageHeader balance={screenSize === ScreenSize.Mobile}> <Box grow="Yes" gap="300"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <Box shrink="No" alignItems="Center"> + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + </Box> + )} + </BackRouteHandler> + )} <Box grow="Yes" alignItems="Center" gap="300"> - <Avatar size="300"> - <RoomAvatar - roomId={room.roomId} - src={avatarUrl} - alt={name} - renderFallback={() => ( - <RoomIcon size="200" joinRule={room.getJoinRule() ?? JoinRule.Restricted} filled /> - )} - /> - </Avatar> + {screenSize !== ScreenSize.Mobile && ( + <Avatar size="300"> + <RoomAvatar + roomId={room.roomId} + src={avatarUrl} + alt={name} + renderFallback={() => ( + <RoomIcon + size="200" + joinRule={room.getJoinRule() ?? JoinRule.Restricted} + filled + /> + )} + /> + </Avatar> + )} <Box direction="Column"> <Text size={topic ? 'H5' : 'H3'} truncate> {name} diff --git a/src/app/hooks/useDeviceList.js b/src/app/hooks/useDeviceList.js deleted file mode 100644 index 7daaad1f..00000000 --- a/src/app/hooks/useDeviceList.js +++ /dev/null @@ -1,31 +0,0 @@ -/* eslint-disable import/prefer-default-export */ -import { useState, useEffect } from 'react'; -import { useMatrixClient } from './useMatrixClient'; - -export function useDeviceList() { - const mx = useMatrixClient(); - const [deviceList, setDeviceList] = useState(null); - - useEffect(() => { - let isMounted = true; - - const updateDevices = () => mx.getDevices().then((data) => { - if (!isMounted) return; - setDeviceList(data.devices || []); - }); - updateDevices(); - - const handleDevicesUpdate = (users) => { - if (users.includes(mx.getUserId())) { - updateDevices(); - } - }; - - mx.on('crypto.devicesUpdated', handleDevicesUpdate); - return () => { - mx.removeListener('crypto.devicesUpdated', handleDevicesUpdate); - isMounted = false; - }; - }, [mx]); - return deviceList; -} diff --git a/src/app/hooks/useDeviceList.ts b/src/app/hooks/useDeviceList.ts new file mode 100644 index 00000000..daec7cbe --- /dev/null +++ b/src/app/hooks/useDeviceList.ts @@ -0,0 +1,35 @@ +/* eslint-disable import/prefer-default-export */ +import { useState, useEffect } from 'react'; +import { CryptoEvent, IMyDevice } from 'matrix-js-sdk'; +import { CryptoEventHandlerMap } from 'matrix-js-sdk/lib/crypto'; +import { useMatrixClient } from './useMatrixClient'; + +export function useDeviceList() { + const mx = useMatrixClient(); + const [deviceList, setDeviceList] = useState<IMyDevice[] | null>(null); + + useEffect(() => { + let isMounted = true; + + const updateDevices = () => + mx.getDevices().then((data) => { + if (!isMounted) return; + setDeviceList(data.devices || []); + }); + updateDevices(); + + const handleDevicesUpdate: CryptoEventHandlerMap[CryptoEvent.DevicesUpdated] = (users) => { + const userId = mx.getUserId(); + if (userId && users.includes(userId)) { + updateDevices(); + } + }; + + mx.on(CryptoEvent.DevicesUpdated, handleDevicesUpdate); + return () => { + mx.removeListener(CryptoEvent.DevicesUpdated, handleDevicesUpdate); + isMounted = false; + }; + }, [mx]); + return deviceList; +} diff --git a/src/app/pages/client/SidebarNav.tsx b/src/app/pages/client/SidebarNav.tsx index fb6bd742..110e4694 100644 --- a/src/app/pages/client/SidebarNav.tsx +++ b/src/app/pages/client/SidebarNav.tsx @@ -10,7 +10,15 @@ import { SidebarItemTooltip, SidebarItem, } from '../../components/sidebar'; -import { DirectTab, HomeTab, SpaceTabs, InboxTab, ExploreTab, UserTab } from './sidebar'; +import { + DirectTab, + HomeTab, + SpaceTabs, + InboxTab, + ExploreTab, + UserTab, + UnverifiedTab, +} from './sidebar'; import { openCreateRoom, openSearch } from '../../../client/action/navigation'; export function SidebarNav() { @@ -65,6 +73,8 @@ export function SidebarNav() { </SidebarItemTooltip> </SidebarItem> + <UnverifiedTab /> + <InboxTab /> <UserTab /> </SidebarStack> diff --git a/src/app/pages/client/explore/Featured.tsx b/src/app/pages/client/explore/Featured.tsx index 4838127f..f056cbb5 100644 --- a/src/app/pages/client/explore/Featured.tsx +++ b/src/app/pages/client/explore/Featured.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Icon, Icons, Scroll, Text } from 'folds'; +import { Box, Icon, IconButton, Icons, Scroll, Text } from 'folds'; import { useAtomValue } from 'jotai'; import { useClientConfig } from '../../../hooks/useClientConfig'; import { RoomCard, RoomCardGrid } from '../../../components/room-card'; @@ -9,21 +9,38 @@ import { Page, PageContent, PageContentCenter, + PageHeader, PageHero, PageHeroSection, } from '../../../components/page'; import { RoomTopicViewer } from '../../../components/room-topic-viewer'; import * as css from './style.css'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../../components/BackRouteHandler'; export function FeaturedRooms() { const { featuredCommunities } = useClientConfig(); const { rooms, spaces } = featuredCommunities ?? {}; const allRooms = useAtomValue(allRoomsAtom); + const screenSize = useScreenSizeContext(); const { navigateSpace, navigateRoom } = useRoomNavigate(); return ( <Page> + {screenSize === ScreenSize.Mobile && ( + <PageHeader> + <Box shrink="No"> + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + </Box> + </PageHeader> + )} <Box grow="Yes"> <Scroll hideTrack visibility="Hover"> <PageContent> diff --git a/src/app/pages/client/explore/Server.tsx b/src/app/pages/client/explore/Server.tsx index 1a81c225..1f493df1 100644 --- a/src/app/pages/client/explore/Server.tsx +++ b/src/app/pages/client/explore/Server.tsx @@ -13,6 +13,7 @@ import { Button, Chip, Icon, + IconButton, Icons, Input, Line, @@ -42,6 +43,8 @@ import { allRoomsAtom } from '../../../state/room-list/roomList'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { getMxIdServer } from '../../../utils/matrix'; import { stopPropagation } from '../../../utils/keyboard'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../../components/BackRouteHandler'; const useServerSearchParams = (searchParams: URLSearchParams): ExploreServerPathSearchParams => useMemo( @@ -344,6 +347,7 @@ export function PublicRooms() { const userServer = userId && getMxIdServer(userId); const allRooms = useAtomValue(allRoomsAtom); const { navigateSpace, navigateRoom } = useRoomNavigate(); + const screenSize = useScreenSizeContext(); const [searchParams] = useSearchParams(); const serverSearchParams = useServerSearchParams(searchParams); @@ -466,7 +470,7 @@ export function PublicRooms() { return ( <Page> - <PageHeader> + <PageHeader balance> {isSearch ? ( <> <Box grow="Yes" basis="No"> @@ -482,20 +486,34 @@ export function PublicRooms() { </Box> <Box grow="No" justifyContent="Center" alignItems="Center" gap="200"> - <Icon size="400" src={Icons.Search} /> + {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />} <Text size="H3" truncate> Search </Text> </Box> - <Box grow="Yes" /> + <Box grow="Yes" basis="No" /> </> ) : ( - <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> - <Icon size="400" src={Icons.Category} /> - <Text size="H3" truncate> - {server} - </Text> - </Box> + <> + <Box grow="Yes" basis="No"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + )} + </Box> + <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> + {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Category} />} + <Text size="H3" truncate> + {server} + </Text> + </Box> + <Box grow="Yes" basis="No" /> + </> )} </PageHeader> <Box grow="Yes"> diff --git a/src/app/pages/client/home/Search.tsx b/src/app/pages/client/home/Search.tsx index af7b1eb9..d5ddfb77 100644 --- a/src/app/pages/client/home/Search.tsx +++ b/src/app/pages/client/home/Search.tsx @@ -1,21 +1,38 @@ import React, { useRef } from 'react'; -import { Box, Icon, Icons, Text, Scroll } from 'folds'; +import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds'; import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page'; import { MessageSearch } from '../../../features/message-search'; import { useHomeRooms } from './useHomeRooms'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../../components/BackRouteHandler'; export function HomeSearch() { const scrollRef = useRef<HTMLDivElement>(null); const rooms = useHomeRooms(); + const screenSize = useScreenSizeContext(); return ( <Page> - <PageHeader> - <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> - <Icon size="400" src={Icons.Search} /> - <Text size="H3" truncate> - Message Search - </Text> + <PageHeader balance> + <Box grow="Yes" alignItems="Center" gap="200"> + <Box grow="Yes" basis="No"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + )} + </Box> + <Box justifyContent="Center" alignItems="Center" gap="200"> + {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />} + <Text size="H3" truncate> + Message Search + </Text> + </Box> + <Box grow="Yes" basis="No" /> </Box> </PageHeader> <Box style={{ position: 'relative' }} grow="Yes"> diff --git a/src/app/pages/client/inbox/Invites.tsx b/src/app/pages/client/inbox/Invites.tsx index 06e5f6c6..18993081 100644 --- a/src/app/pages/client/inbox/Invites.tsx +++ b/src/app/pages/client/inbox/Invites.tsx @@ -4,6 +4,7 @@ import { Box, Button, Icon, + IconButton, Icons, Overlay, OverlayBackdrop, @@ -39,6 +40,8 @@ import { RoomTopicViewer } from '../../../components/room-topic-viewer'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useRoomNavigate } from '../../../hooks/useRoomNavigate'; import { useRoomTopic } from '../../../hooks/useRoomMeta'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../../components/BackRouteHandler'; const COMPACT_CARD_WIDTH = 548; @@ -205,6 +208,7 @@ export function Invites() { useCallback(() => containerRef.current, []), useCallback((width) => setCompact(width <= COMPACT_CARD_WIDTH), []) ); + const screenSize = useScreenSizeContext(); const { navigateRoom, navigateSpace } = useRoomNavigate(); @@ -225,12 +229,26 @@ export function Invites() { return ( <Page> - <PageHeader> - <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> - <Icon size="400" src={Icons.Mail} /> - <Text size="H3" truncate> - Invitations - </Text> + <PageHeader balance> + <Box grow="Yes" gap="200"> + <Box grow="Yes" basis="No"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + )} + </Box> + <Box alignItems="Center" gap="200"> + {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Mail} />} + <Text size="H3" truncate> + Invitations + </Text> + </Box> + <Box grow="Yes" basis="No" /> </Box> </PageHeader> <Box grow="Yes"> diff --git a/src/app/pages/client/inbox/Notifications.tsx b/src/app/pages/client/inbox/Notifications.tsx index 3425b519..6a8160d8 100644 --- a/src/app/pages/client/inbox/Notifications.tsx +++ b/src/app/pages/client/inbox/Notifications.tsx @@ -78,6 +78,8 @@ import { UserAvatar } from '../../../components/user-avatar'; import { EncryptedContent } from '../../../features/room/message'; import { useMentionClickHandler } from '../../../hooks/useMentionClickHandler'; import { useSpoilerClickHandler } from '../../../hooks/useSpoilerClickHandler'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../../components/BackRouteHandler'; type RoomNotificationsGroup = { roomId: string; @@ -484,6 +486,7 @@ export function Notifications() { const mx = useMatrixClient(); const [mediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [urlPreview] = useSetting(settingsAtom, 'urlPreview'); + const screenSize = useScreenSizeContext(); const { navigateRoom } = useRoomNavigate(); const [searchParams, setSearchParams] = useSearchParams(); @@ -549,12 +552,26 @@ export function Notifications() { return ( <Page> - <PageHeader> - <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> - <Icon size="400" src={Icons.Message} /> - <Text size="H3" truncate> - Notification Messages - </Text> + <PageHeader balance> + <Box grow="Yes" gap="200"> + <Box grow="Yes" basis="No"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + )} + </Box> + <Box alignItems="Center" gap="200"> + {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Message} />} + <Text size="H3" truncate> + Notification Messages + </Text> + </Box> + <Box grow="Yes" basis="No" /> </Box> </PageHeader> diff --git a/src/app/pages/client/sidebar/UnverifiedTab.css.ts b/src/app/pages/client/sidebar/UnverifiedTab.css.ts new file mode 100644 index 00000000..e8fe8c51 --- /dev/null +++ b/src/app/pages/client/sidebar/UnverifiedTab.css.ts @@ -0,0 +1,24 @@ +import { keyframes, style } from '@vanilla-extract/css'; +import { color, toRem } from 'folds'; + +const pushRight = keyframes({ + from: { + transform: `translateX(${toRem(2)}) scale(1)`, + }, + to: { + transform: 'translateX(0) scale(1)', + }, +}); + +export const UnverifiedTab = style({ + animationName: pushRight, + animationDuration: '400ms', + animationIterationCount: 30, + animationDirection: 'alternate', +}); + +export const UnverifiedAvatar = style({ + backgroundColor: color.Critical.Container, + color: color.Critical.OnContainer, + borderColor: color.Critical.ContainerLine, +}); diff --git a/src/app/pages/client/sidebar/UnverifiedTab.tsx b/src/app/pages/client/sidebar/UnverifiedTab.tsx new file mode 100644 index 00000000..402ffc2b --- /dev/null +++ b/src/app/pages/client/sidebar/UnverifiedTab.tsx @@ -0,0 +1,51 @@ +import React from 'react'; +import { Badge, color, Icon, Icons, Text } from 'folds'; +import { openSettings } from '../../../../client/action/navigation'; +import { isCrossVerified } from '../../../../util/matrixUtil'; +import { + SidebarAvatar, + SidebarItem, + SidebarItemBadge, + SidebarItemTooltip, +} from '../../../components/sidebar'; +import { useDeviceList } from '../../../hooks/useDeviceList'; +import { tabText } from '../../../organisms/settings/Settings'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import * as css from './UnverifiedTab.css'; + +export function UnverifiedTab() { + const mx = useMatrixClient(); + const deviceList = useDeviceList(); + console.log(deviceList); + const unverified = deviceList?.filter( + (device) => isCrossVerified(mx, device.device_id) === false + ); + console.log(unverified); + + if (!unverified?.length) return null; + + return ( + <SidebarItem className={css.UnverifiedTab}> + <SidebarItemTooltip tooltip="Unverified Sessions"> + {(triggerRef) => ( + <SidebarAvatar + className={css.UnverifiedAvatar} + as="button" + ref={triggerRef} + outlined + onClick={() => openSettings(tabText.SECURITY)} + > + <Icon style={{ color: color.Critical.Main }} src={Icons.ShieldUser} /> + </SidebarAvatar> + )} + </SidebarItemTooltip> + <SidebarItemBadge hasCount> + <Badge variant="Critical" size="400" fill="Solid" radii="Pill" outlined={false}> + <Text as="span" size="L400"> + {unverified.length} + </Text> + </Badge> + </SidebarItemBadge> + </SidebarItem> + ); +} diff --git a/src/app/pages/client/sidebar/index.ts b/src/app/pages/client/sidebar/index.ts index 63c5d4bb..5da8780b 100644 --- a/src/app/pages/client/sidebar/index.ts +++ b/src/app/pages/client/sidebar/index.ts @@ -4,3 +4,4 @@ export * from './SpaceTabs'; export * from './InboxTab'; export * from './ExploreTab'; export * from './UserTab'; +export * from './UnverifiedTab'; diff --git a/src/app/pages/client/space/Search.tsx b/src/app/pages/client/space/Search.tsx index 6e7ac57d..017262b5 100644 --- a/src/app/pages/client/space/Search.tsx +++ b/src/app/pages/client/space/Search.tsx @@ -1,5 +1,5 @@ import React, { useRef } from 'react'; -import { Box, Icon, Icons, Text, Scroll } from 'folds'; +import { Box, Icon, Icons, Text, Scroll, IconButton } from 'folds'; import { useAtomValue } from 'jotai'; import { Page, PageContent, PageContentCenter, PageHeader } from '../../../components/page'; import { MessageSearch } from '../../../features/message-search'; @@ -9,11 +9,14 @@ import { allRoomsAtom } from '../../../state/room-list/roomList'; import { mDirectAtom } from '../../../state/mDirectList'; import { roomToParentsAtom } from '../../../state/room/roomToParents'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { ScreenSize, useScreenSizeContext } from '../../../hooks/useScreenSize'; +import { BackRouteHandler } from '../../../components/BackRouteHandler'; export function SpaceSearch() { const mx = useMatrixClient(); const scrollRef = useRef<HTMLDivElement>(null); const space = useSpace(); + const screenSize = useScreenSizeContext(); const mDirects = useAtomValue(mDirectAtom); const roomToParents = useAtomValue(roomToParentsAtom); @@ -25,12 +28,26 @@ export function SpaceSearch() { return ( <Page> - <PageHeader> - <Box grow="Yes" justifyContent="Center" alignItems="Center" gap="200"> - <Icon size="400" src={Icons.Search} /> - <Text size="H3" truncate> - Message Search - </Text> + <PageHeader balance> + <Box grow="Yes" alignItems="Center" gap="200"> + <Box grow="Yes" basis="No"> + {screenSize === ScreenSize.Mobile && ( + <BackRouteHandler> + {(onBack) => ( + <IconButton onClick={onBack}> + <Icon src={Icons.ArrowLeft} /> + </IconButton> + )} + </BackRouteHandler> + )} + </Box> + <Box justifyContent="Center" alignItems="Center" gap="200"> + {screenSize !== ScreenSize.Mobile && <Icon size="400" src={Icons.Search} />} + <Text size="H3" truncate> + Message Search + </Text> + </Box> + <Box grow="Yes" basis="No" /> </Box> </PageHeader> <Box style={{ position: 'relative' }} grow="Yes"> diff --git a/src/util/matrixUtil.js b/src/util/matrixUtil.js index 9f1d9421..7664c3a4 100644 --- a/src/util/matrixUtil.js +++ b/src/util/matrixUtil.js @@ -106,7 +106,7 @@ export function isCrossVerified(mx, deviceId) { const deviceInfo = mx.getStoredDevice(mx.getUserId(), deviceId); const deviceTrust = crossSignInfo.checkDeviceTrust(crossSignInfo, deviceInfo, false, true); return deviceTrust.isCrossSigningVerified(); - } catch { + } catch (e) { // device does not support encryption return null; }