From 020c9355fb750f37bafa823e995bd4590c2a6de6 Mon Sep 17 00:00:00 2001 From: "v.lagerev" Date: Sun, 26 Apr 2026 23:34:20 +0300 Subject: [PATCH] redesign(p2): rebuild DM list panel with stream header, segmented tabs, self-row, new-chat row, footer status, and live timeline rerender --- public/locales/en.json | 8 + public/locales/ru.json | 8 + src/app/components/page/style.css.ts | 3 + src/app/features/room-nav/DmStreamRow.tsx | 513 ++++++++++++++++++ src/app/features/room-nav/index.ts | 1 + src/app/pages/client/direct/Direct.tsx | 264 +++------ .../pages/client/direct/DirectNewChatRow.tsx | 56 ++ src/app/pages/client/direct/DirectSelfRow.tsx | 127 +++++ .../client/direct/DirectStreamHeader.tsx | 87 +++ 9 files changed, 890 insertions(+), 177 deletions(-) create mode 100644 src/app/features/room-nav/DmStreamRow.tsx create mode 100644 src/app/pages/client/direct/DirectNewChatRow.tsx create mode 100644 src/app/pages/client/direct/DirectSelfRow.tsx create mode 100644 src/app/pages/client/direct/DirectStreamHeader.tsx diff --git a/public/locales/en.json b/public/locales/en.json index ca7175a8..0bf537f6 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -370,6 +370,14 @@ "direct_message": "Direct Message", "create_chat": "Create Chat", "create_chat_subtitle": "Start a private, encrypted chat by entering a username.", + "start_first_chat": "Start a chat", + "segment_dm": "DM", + "segment_channels": "Channels", + "segment_bots": "Bots", + "segment_coming_soon": "Coming soon", + "self_row_label": "You", + "self_row_preview": "Settings & profile", + "status_e2ee": "e2ee", "chats": "Chats", "username": "Username", "username_placeholder": "username", diff --git a/public/locales/ru.json b/public/locales/ru.json index 2c7f31cb..be007877 100644 --- a/public/locales/ru.json +++ b/public/locales/ru.json @@ -370,6 +370,14 @@ "direct_message": "Новый чат", "create_chat": "Новый чат", "create_chat_subtitle": "Начните приватный зашифрованный чат, указав имя пользователя.", + "start_first_chat": "Начать чат", + "segment_dm": "Личные", + "segment_channels": "Каналы", + "segment_bots": "Боты", + "segment_coming_soon": "Скоро", + "self_row_label": "Я", + "self_row_preview": "Настройки и профиль", + "status_e2ee": "e2ee", "chats": "Чаты", "username": "Имя пользователя", "username_placeholder": "username", diff --git a/src/app/components/page/style.css.ts b/src/app/components/page/style.css.ts index bd14cd58..18dc491a 100644 --- a/src/app/components/page/style.css.ts +++ b/src/app/components/page/style.css.ts @@ -5,6 +5,9 @@ import { DefaultReset, color, config, toRem } from 'folds'; export const PageNav = recipe({ variants: { size: { + '500': { + width: toRem(320), + }, '400': { width: toRem(256), }, diff --git a/src/app/features/room-nav/DmStreamRow.tsx b/src/app/features/room-nav/DmStreamRow.tsx new file mode 100644 index 00000000..2faea0b5 --- /dev/null +++ b/src/app/features/room-nav/DmStreamRow.tsx @@ -0,0 +1,513 @@ +import React, { MouseEventHandler, forwardRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Room } from 'matrix-js-sdk/lib/models/room'; +import { + Avatar, + Badge, + Box, + Icon, + IconButton, + Icons, + Line, + Menu, + MenuItem, + PopOut, + RectCords, + Spinner, + Text, + color, + config, + toRem, +} from 'folds'; +import { useFocusWithin, useHover } from 'react-aria'; +import FocusTrap from 'focus-trap-react'; +import { useAtom, useAtomValue } from 'jotai'; +import { NavItem, NavItemContent, NavItemOptions, NavLink } from '../../components/nav'; +import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge'; +import { RoomAvatar } from '../../components/room-avatar'; +import { getDirectRoomAvatarUrl } from '../../utils/room'; +import { nameInitials } from '../../utils/common'; +import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useRoomUnread } from '../../state/hooks/unread'; +import { roomToUnreadAtom } from '../../state/room/roomToUnread'; +import { usePowerLevels } from '../../hooks/usePowerLevels'; +import { copyToClipboard } from '../../utils/dom'; +import { markAsRead } from '../../utils/notifications'; +import { UseStateProvider } from '../../components/UseStateProvider'; +import { LeaveRoomPrompt } from '../../components/leave-room-prompt'; +import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; +import { TypingIndicator } from '../../components/typing-indicator'; +import { stopPropagation } from '../../utils/keyboard'; +import { getMatrixToRoom } from '../../plugins/matrix-to'; +import { getCanonicalAliasOrRoomId, isRoomAlias } from '../../utils/matrix'; +import { getViaServers } from '../../plugins/via-servers'; +import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; +import { useOpenRoomSettings } from '../../state/hooks/roomSettings'; +import { useSpaceOptionally } from '../../hooks/useSpace'; +import { + RoomNotificationMode, + getRoomNotificationModeIcon, +} from '../../hooks/useRoomsNotificationPreferences'; +import { RoomNotificationModeSwitcher } from '../../components/RoomNotificationSwitcher'; +import { useRoomCreators } from '../../hooks/useRoomCreators'; +import { useRoomPermissions } from '../../hooks/useRoomPermissions'; +import { InviteUserPrompt } from '../../components/invite-user-prompt'; +import { useRoomName } from '../../hooks/useRoomMeta'; +import { useCallMembers, useCallSession } from '../../hooks/useCall'; +import { useCallEmbed, useCallStart } from '../../hooks/useCallEmbed'; +import { callChatAtom } from '../../state/callEmbed'; +import { useCallPreferencesAtom } from '../../state/hooks/callPreferences'; +import { useAutoDiscoveryInfo } from '../../hooks/useAutoDiscoveryInfo'; +import { livekitSupport } from '../../hooks/useLivekitSupport'; +import { timeHourMinute } from '../../utils/time'; + +const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace'; +const ROW_MIN_HEIGHT = toRem(68); + +type DmStreamRowMenuProps = { + room: Room; + requestClose: () => void; + notificationMode?: RoomNotificationMode; +}; +const DmStreamRowMenu = forwardRef( + ({ room, requestClose, notificationMode }, ref) => { + const { t } = useTranslation(); + const mx = useMatrixClient(); + const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); + const unread = useRoomUnread(room.roomId, roomToUnreadAtom); + const powerLevels = usePowerLevels(room); + const creators = useRoomCreators(room); + + const permissions = useRoomPermissions(creators, powerLevels); + const canInvite = permissions.action('invite', mx.getSafeUserId()); + const openRoomSettings = useOpenRoomSettings(); + const space = useSpaceOptionally(); + + const [invitePrompt, setInvitePrompt] = useState(false); + + const handleMarkAsRead = () => { + markAsRead(mx, room.roomId, hideActivity); + requestClose(); + }; + + const handleInvite = () => { + setInvitePrompt(true); + }; + + const handleCopyLink = () => { + const roomIdOrAlias = getCanonicalAliasOrRoomId(mx, room.roomId); + const viaServers = isRoomAlias(roomIdOrAlias) ? undefined : getViaServers(room); + copyToClipboard(getMatrixToRoom(roomIdOrAlias, viaServers)); + requestClose(); + }; + + const handleRoomSettings = () => { + openRoomSettings(room.roomId, space?.roomId); + requestClose(); + }; + + return ( + + {invitePrompt && room && ( + { + setInvitePrompt(false); + requestClose(); + }} + /> + )} + + } + radii="300" + disabled={!unread} + > + + {t('Home.mark_as_read')} + + + + {(handleOpen, opened, changing) => ( + + ) : ( + + ) + } + radii="300" + aria-pressed={opened} + onClick={handleOpen} + > + + {t('Home.notifications')} + + + )} + + + + + } + radii="300" + aria-pressed={invitePrompt} + disabled={!canInvite} + > + + {t('Home.invite')} + + + } + radii="300" + > + + {t('Home.copy_link')} + + + } + radii="300" + > + + {t('Home.room_settings')} + + + + + + + {(promptLeave, setPromptLeave) => ( + <> + setPromptLeave(true)} + variant="Critical" + fill="None" + size="300" + after={} + radii="300" + aria-pressed={promptLeave} + > + + {t('Home.leave_room')} + + + {promptLeave && ( + setPromptLeave(false)} + /> + )} + + )} + + + + ); + } +); + +function CallChatToggle() { + const { t } = useTranslation(); + const [chat, setChat] = useAtom(callChatAtom); + + return ( + setChat(!chat)} + aria-pressed={chat} + aria-label={t('Home.toggle_chat')} + variant="Background" + fill="None" + size="300" + radii="300" + > + + + ); +} + +type RoomPreview = { ts: number | undefined; text: string }; + +const deriveRoomPreview = (room: Room): RoomPreview => { + const events = room.getLiveTimeline().getEvents(); + for (let i = events.length - 1; i >= 0; i -= 1) { + const ev = events[i]; + if (ev.getType() === 'm.room.message' && !ev.isRedacted()) { + const content = ev.getContent(); + const body = typeof content?.body === 'string' ? content.body.trim() : ''; + if (body) { + return { ts: ev.getTs(), text: body.replace(/\s+/g, ' ') }; + } + } + } + return { ts: room.getLastActiveTimestamp() || undefined, text: '' }; +}; + +const formatRowTime = (ts: number, hour24Clock: boolean): string => { + const now = Date.now(); + const diff = now - ts; + const day = 24 * 60 * 60 * 1000; + const date = new Date(ts); + const today = new Date(); + const sameDay = + date.getFullYear() === today.getFullYear() && + date.getMonth() === today.getMonth() && + date.getDate() === today.getDate(); + if (sameDay) return timeHourMinute(ts, hour24Clock); + if (diff < 7 * day) { + return date.toLocaleDateString(undefined, { weekday: 'short' }); + } + return date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' }); +}; + +type DmStreamRowProps = { + room: Room; + selected: boolean; + linkPath: string; + notificationMode?: RoomNotificationMode; +}; +export function DmStreamRow({ room, selected, notificationMode, linkPath }: DmStreamRowProps) { + const { t } = useTranslation(); + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const [hover, setHover] = useState(false); + const { hoverProps } = useHover({ onHoverChange: setHover }); + const { focusWithinProps } = useFocusWithin({ onFocusWithinChange: setHover }); + const [menuAnchor, setMenuAnchor] = useState(); + const unread = useRoomUnread(room.roomId, roomToUnreadAtom); + const typingMember = useRoomTypingMember(room.roomId).filter( + (receipt) => receipt.userId !== mx.getUserId() + ); + + const roomName = useRoomName(room); + + const preview = deriveRoomPreview(room); + const previewText = preview.text; + const previewTs = preview.ts; + const timeLabel = previewTs ? formatRowTime(previewTs, hour24Clock) : ''; + + const handleContextMenu: MouseEventHandler = (evt) => { + evt.preventDefault(); + setMenuAnchor({ + x: evt.clientX, + y: evt.clientY, + width: 0, + height: 0, + }); + }; + + const handleOpenMenu: MouseEventHandler = (evt) => { + setMenuAnchor(evt.currentTarget.getBoundingClientRect()); + }; + + const optionsVisible = hover || !!menuAnchor; + const callSession = useCallSession(room); + const callMembers = useCallMembers(room, callSession); + const startCall = useCallStart(true); + const callEmbed = useCallEmbed(); + const callPref = useAtomValue(useCallPreferencesAtom()); + const autoDiscoveryInfo = useAutoDiscoveryInfo(); + + const handleStartCall: MouseEventHandler = (evt) => { + if (!livekitSupport(autoDiscoveryInfo) && callMembers.length === 0) { + return; + } + if (callEmbed) { + return; + } + if (selected) { + evt.preventDefault(); + startCall(room, callPref); + } + }; + + return ( + + + + + + ( + + {nameInitials(roomName)} + + )} + /> + + + + + + {roomName} + + + {timeLabel && !optionsVisible && ( + + {timeLabel} + + )} + + + + {typingMember.length > 0 ? ( + + + + ) : ( + + {previewText} + + )} + + {!optionsVisible && unread && ( + + 0} count={unread.total} /> + + )} + {!optionsVisible && notificationMode !== RoomNotificationMode.Unset && ( + + )} + {room.isCallRoom() && callMembers.length > 0 && ( + + + {t('Home.live_count', { count: callMembers.length })} + + + )} + + + + + + {optionsVisible && ( + + {selected && (callEmbed?.roomId === room.roomId || room.isCallRoom()) && ( + + )} + setMenuAnchor(undefined), + clickOutsideDeactivates: true, + isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', + isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', + escapeDeactivates: stopPropagation, + }} + > + setMenuAnchor(undefined)} + notificationMode={notificationMode} + /> + + } + > + + + + + + )} + + ); +} diff --git a/src/app/features/room-nav/index.ts b/src/app/features/room-nav/index.ts index d1ea3ec7..e58899a7 100644 --- a/src/app/features/room-nav/index.ts +++ b/src/app/features/room-nav/index.ts @@ -1,2 +1,3 @@ export * from './RoomNavItem'; export * from './RoomNavCategoryButton'; +export * from './DmStreamRow'; diff --git a/src/app/pages/client/direct/Direct.tsx b/src/app/pages/client/direct/Direct.tsx index a5802437..ce76c3e3 100644 --- a/src/app/pages/client/direct/Direct.tsx +++ b/src/app/pages/client/direct/Direct.tsx @@ -1,145 +1,30 @@ -import React, { MouseEventHandler, forwardRef, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useAtom, useAtomValue } from 'jotai'; -import { - Avatar, - Box, - Button, - Icon, - IconButton, - Icons, - Menu, - MenuItem, - PopOut, - RectCords, - Text, - config, - toRem, -} from 'folds'; +import { useAtomValue } from 'jotai'; +import { Box, Button, Icon, Icons, Text, color, config, toRem } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; -import FocusTrap from 'focus-trap-react'; import { useNavigate } from 'react-router-dom'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { factoryRoomIdByActivity } from '../../../utils/sort'; -import { - NavButton, - NavCategory, - NavCategoryHeader, - NavEmptyCenter, - NavEmptyLayout, - NavItem, - NavItemContent, -} from '../../../components/nav'; +import { NavCategory, NavEmptyCenter, NavEmptyLayout } from '../../../components/nav'; import { getDirectCreatePath, getDirectRoomPath } from '../../pathUtils'; import { getCanonicalAliasOrRoomId } from '../../../utils/matrix'; import { useSelectedRoom } from '../../../hooks/router/useSelectedRoom'; import { VirtualTile } from '../../../components/virtualizer'; -import { RoomNavCategoryButton, RoomNavItem } from '../../../features/room-nav'; -import { makeNavCategoryId } from '../../../state/closedNavCategories'; +import { DmStreamRow } from '../../../features/room-nav'; import { roomToUnreadAtom } from '../../../state/room/roomToUnread'; -import { useCategoryHandler } from '../../../hooks/useCategoryHandler'; import { useNavToActivePathMapper } from '../../../hooks/useNavToActivePathMapper'; import { useDirectRooms } from './useDirectRooms'; -import { PageNav, PageNavContent, PageNavHeader } from '../../../components/page'; -import { useClosedNavCategoriesAtom } from '../../../state/hooks/closedNavCategories'; -import { useRoomsUnread } from '../../../state/hooks/unread'; -import { markAsRead } from '../../../utils/notifications'; -import { stopPropagation } from '../../../utils/keyboard'; -import { useSetting } from '../../../state/hooks/settings'; -import { settingsAtom } from '../../../state/settings'; +import { PageNav, PageNavContent } from '../../../components/page'; import { getRoomNotificationMode, useRoomsNotificationPreferencesContext, } from '../../../hooks/useRoomsNotificationPreferences'; -import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected'; +import { DirectStreamHeader } from './DirectStreamHeader'; +import { DirectNewChatRow } from './DirectNewChatRow'; +import { DirectSelfRow } from './DirectSelfRow'; -type DirectMenuProps = { - requestClose: () => void; -}; -const DirectMenu = forwardRef(({ requestClose }, ref) => { - const { t } = useTranslation(); - const mx = useMatrixClient(); - const [hideActivity] = useSetting(settingsAtom, 'hideActivity'); - const orphanRooms = useDirectRooms(); - const unread = useRoomsUnread(orphanRooms, roomToUnreadAtom); - - const handleMarkAsRead = () => { - if (!unread) return; - orphanRooms.forEach((rId) => markAsRead(mx, rId, hideActivity)); - requestClose(); - }; - - return ( - - - } - radii="300" - aria-disabled={!unread} - > - - {t('Direct.mark_as_read')} - - - - - ); -}); - -function DirectHeader() { - const { t } = useTranslation(); - const [menuAnchor, setMenuAnchor] = useState(); - - const handleOpenMenu: MouseEventHandler = (evt) => { - const cords = evt.currentTarget.getBoundingClientRect(); - setMenuAnchor((currentState) => { - if (currentState) return undefined; - return cords; - }); - }; - - return ( - <> - - - - - {t('Direct.direct_messages')} - - - - - - - - - - setMenuAnchor(undefined), - clickOutsideDeactivates: true, - isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown', - isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp', - escapeDeactivates: stopPropagation, - }} - > - setMenuAnchor(undefined)} /> - - } - /> - - ); -} +const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace'; function DirectEmpty() { const { t } = useTranslation(); @@ -160,9 +45,9 @@ function DirectEmpty() { } options={ - } @@ -171,78 +56,102 @@ function DirectEmpty() { ); } -const DEFAULT_CATEGORY_ID = makeNavCategoryId('direct', 'direct'); -export function Direct() { +function DirectFooterStatus() { const { t } = useTranslation(); + return ( + + + vojo.chat + + {t('Direct.status_e2ee')} + + ); +} + +export function Direct() { const mx = useMatrixClient(); useNavToActivePathMapper('direct'); const scrollRef = useRef(null); const directs = useDirectRooms(); const notificationPreferences = useRoomsNotificationPreferencesContext(); - const roomToUnread = useAtomValue(roomToUnreadAtom); - const navigate = useNavigate(); + // roomToUnreadAtom only changes on read/unread transitions and ignores own + // events — covers incoming notifying messages but not own sends or muted + // incoming. Kept as a subscribe-only re-render trigger. + useAtomValue(roomToUnreadAtom); - const createDirectSelected = useDirectCreateSelected(); + // Activity tick: bump on every live timeline event in any DM room so list + // order, preview text and time labels refresh on own sends, muted incoming, + // reactions — anything roomToUnread misses. Uses the 'Room.timeline' + // literal (= RoomEvent.Timeline) to avoid named imports from matrix-js-sdk + // that the project's moduleResolution flags as TS2614 (see + // docs/known-tech-debt-lint/). + const [, setActivityTick] = useState(0); + useEffect(() => { + const directsSet = new Set(directs); + const handleTimeline = ( + _ev: unknown, + room: { roomId: string } | undefined, + _start: unknown, + _removed: unknown, + data: { liveEvent?: boolean } | undefined + ) => { + if (!room || !data?.liveEvent) return; + if (!directsSet.has(room.roomId)) return; + setActivityTick((n) => n + 1); + }; + const emitter = mx as unknown as { + on: (e: string, h: typeof handleTimeline) => void; + removeListener: (e: string, h: typeof handleTimeline) => void; + }; + emitter.on('Room.timeline', handleTimeline); + return () => { + emitter.removeListener('Room.timeline', handleTimeline); + }; + }, [mx, directs]); const selectedRoomId = useSelectedRoom(); const noRoomToDisplay = directs.length === 0; - const [closedCategories, setClosedCategories] = useAtom(useClosedNavCategoriesAtom()); - const sortedDirects = useMemo(() => { - const items = Array.from(directs).sort(factoryRoomIdByActivity(mx)); - if (closedCategories.has(DEFAULT_CATEGORY_ID)) { - return items.filter((rId) => roomToUnread.has(rId) || rId === selectedRoomId); - } - return items; - }, [mx, directs, closedCategories, roomToUnread, selectedRoomId]); + // Sort each render — small list, getLastActiveTimestamp changes outside + // React's dep model so memoising would need a manual trigger anyway. + const sortedDirects = Array.from(directs).sort(factoryRoomIdByActivity(mx)); const virtualizer = useVirtualizer({ count: sortedDirects.length, getScrollElement: () => scrollRef.current, - estimateSize: () => 38, + estimateSize: () => 68, overscan: 10, }); - const handleCategoryClick = useCategoryHandler(setClosedCategories, (categoryId) => - closedCategories.has(categoryId) - ); - return ( - - + + {noRoomToDisplay ? ( ) : ( - - navigate(getDirectCreatePath())}> - - - - - - - - {t('Direct.create_chat')} - - - - - - - - - - - {t('Direct.chats')} - -
- )} + + + ); } diff --git a/src/app/pages/client/direct/DirectNewChatRow.tsx b/src/app/pages/client/direct/DirectNewChatRow.tsx new file mode 100644 index 00000000..6bcb76c1 --- /dev/null +++ b/src/app/pages/client/direct/DirectNewChatRow.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Avatar, Box, Icon, Icons, Text, color, config, toRem } from 'folds'; +import { useNavigate } from 'react-router-dom'; +import { NavButton, NavItem, NavItemContent } from '../../../components/nav'; +import { getDirectCreatePath } from '../../pathUtils'; +import { useDirectCreateSelected } from '../../../hooks/router/useDirectSelected'; + +const ROW_MIN_HEIGHT = toRem(56); + +export function DirectNewChatRow() { + const { t } = useTranslation(); + const navigate = useNavigate(); + const selected = useDirectCreateSelected(); + + return ( + + + navigate(getDirectCreatePath())}> + + + + + + + + {t('Direct.create_chat')} + + + + + + + + ); +} diff --git a/src/app/pages/client/direct/DirectSelfRow.tsx b/src/app/pages/client/direct/DirectSelfRow.tsx new file mode 100644 index 00000000..de599bd3 --- /dev/null +++ b/src/app/pages/client/direct/DirectSelfRow.tsx @@ -0,0 +1,127 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Avatar, Box, Icon, Icons, Text, color, config, toRem } from 'folds'; +import { NavButton, NavItem, NavItemContent } from '../../../components/nav'; +import { UserAvatar } from '../../../components/user-avatar'; +import { useMatrixClient } from '../../../hooks/useMatrixClient'; +import { useUserProfile } from '../../../hooks/useUserProfile'; +import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../../utils/matrix'; +import { nameInitials } from '../../../utils/common'; +import { Settings } from '../../../features/settings'; +import { Modal500 } from '../../../components/Modal500'; + +const MONO_FONT = '"JetBrains Mono Variable", ui-monospace, monospace'; +const ROW_MIN_HEIGHT = toRem(68); + +export function DirectSelfRow() { + const { t } = useTranslation(); + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const userId = mx.getSafeUserId(); + const profile = useUserProfile(userId); + + const [open, setOpen] = useState(false); + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + const displayName = profile.displayName ?? getMxIdLocalPart(userId) ?? userId; + const avatarUrl = profile.avatarUrl + ? mxcUrlToHttp(mx, profile.avatarUrl, useAuthentication, 96, 96, 'crop') ?? undefined + : undefined; + + return ( + <> + + + + + + + ( + + {nameInitials(displayName)} + + )} + /> + + + + + {t('Direct.self_row_label')} + + + {userId} + + + + {t('Direct.self_row_preview')} + + + + + + + + + {open && ( + + + + )} + + ); +} diff --git a/src/app/pages/client/direct/DirectStreamHeader.tsx b/src/app/pages/client/direct/DirectStreamHeader.tsx new file mode 100644 index 00000000..137a01e8 --- /dev/null +++ b/src/app/pages/client/direct/DirectStreamHeader.tsx @@ -0,0 +1,87 @@ +import React, { forwardRef } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Box, Text, Tooltip, TooltipProvider, color, toRem } from 'folds'; +import { PageNavHeader } from '../../../components/page'; + +type SegmentProps = { + active: boolean; + disabled?: boolean; + label: string; + onClick?: () => void; +}; +const Segment = forwardRef( + ({ active, disabled, label, onClick }, ref) => ( + + ) +); + +export function DirectStreamHeader() { + const { t } = useTranslation(); + const comingSoon = t('Direct.segment_coming_soon'); + + return ( + + + + + {comingSoon} + + } + > + {(triggerRef) => ( + } + active={false} + disabled + label={t('Direct.segment_channels')} + /> + )} + + + {comingSoon} + + } + > + {(triggerRef) => ( + } + active={false} + disabled + label={t('Direct.segment_bots')} + /> + )} + + + + ); +}