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, 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, getRoomAvatarUrl, isOneOnOneRoom } 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'; import * as css from './styles.css'; 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): 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); 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 [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) : ''; 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} /> } > )} ); }