import { Box, Button, color, config, Icon, IconButton, Icons, Spinner, Text, toRem } from 'folds'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { VerificationRequest } from 'matrix-js-sdk/lib/crypto-api'; import { AsyncState, AsyncStatus, useAsync } from '../../hooks/useAsyncCallback'; import { VerificationStatus } from '../../hooks/useDeviceVerificationStatus'; import { DeviceVerificationStatus } from '../DeviceVerificationStatus'; import { DeviceVerification } from '../DeviceVerification'; import { useOtherUserDevices, UserDevice } from '../../hooks/useOtherUserDevices'; import { ContainerColor } from '../../styles/ContainerColor.css'; import { UserHero, UserHeroName } from './UserHero'; import { getMxIdServer, mxcUrlToHttp } from '../../utils/matrix'; import { getMemberAvatarMxc, getMemberDisplayName } from '../../utils/room'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { usePowerLevels } from '../../hooks/usePowerLevels'; import { useRoom } from '../../hooks/useRoom'; import { useUserPresence } from '../../hooks/useUserPresence'; import { IgnoredUserAlert, MutualRoomsChip, OptionsChip, ServerChip, ShareChip } from './UserChips'; import { useCrossSigningActive } from '../../hooks/useCrossSigning'; import { MemberVerificationBadge } from '../MemberVerificationBadge'; import { useCloseUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { PowerChip } from './PowerChip'; import { UserInviteAlert, UserBanAlert, UserModeration, UserKickAlert } from './UserModeration'; import { useIgnoredUsers } from '../../hooks/useIgnoredUsers'; import { useMembership } from '../../hooks/useMembership'; import { Membership } from '../../../types/matrix/room'; import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare'; import { ReportUserModal } from '../../features/room/ReportUserModal'; import { CreatorChip } from './CreatorChip'; import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils'; import { DirectCreateSearchParams } from '../../pages/paths'; import { useExtendedProfile } from '../../hooks/useExtendedProfile'; import { useUserNotes, USER_NOTE_MAX_LENGTH } from '../../hooks/useUserNotes'; type VerifyDeviceButtonProps = { userId: string; deviceId: string; }; function VerifyDeviceButton({ userId, deviceId }: VerifyDeviceButtonProps) { const mx = useMatrixClient(); const [requestState, setRequestState] = useState>({ status: AsyncStatus.Idle, }); const requestVerification = useAsync( useCallback(() => { const crypto = mx.getCrypto(); if (!crypto) return Promise.reject(new Error('Crypto unavailable')); return crypto.requestDeviceVerification(userId, deviceId); }, [mx, userId, deviceId]), setRequestState, ); const handleExit = useCallback(() => setRequestState({ status: AsyncStatus.Idle }), []); const requesting = requestState.status === AsyncStatus.Loading; return ( <> {requestState.status === AsyncStatus.Success && ( )} ); } type UserDeviceRowProps = { userId: string; device: UserDevice; }; function UserDeviceRow({ userId, device }: UserDeviceRowProps) { const mx = useMatrixClient(); const crypto = mx.getCrypto() ?? undefined; return ( {(status) => { const deviceColor = status === VerificationStatus.Verified ? color.Success.Main : status === VerificationStatus.Unverified ? color.Warning.Main : color.SurfaceVariant.OnContainer; return ( {device.displayName ?? device.deviceId} {device.displayName && ( {device.deviceId} )} {status === VerificationStatus.Unverified && ( )} ); }} ); } type UserDeviceSessionsProps = { userId: string; }; function UserDeviceSessions({ userId }: UserDeviceSessionsProps) { const devicesState = useOtherUserDevices(userId); const [expanded, setExpanded] = useState(false); if (devicesState.status === 'loading') { return ( Loading sessions… ); } if (devicesState.status === 'error') { return ( Could not load sessions. ); } const devices = devicesState.devices; if (devices.length === 0) return null; return ( Sessions {devices.length} setExpanded((v) => !v)} > {expanded && ( {devices.map((device) => ( ))} )} ); } function UserPrivateNotes({ userId }: { userId: string }) { const { getNote, setNote } = useUserNotes(); const [draft, setDraft] = useState(() => getNote(userId)); const [saving, setSaving] = useState(false); const saveTimer = useRef | undefined>(undefined); // Sync if account data arrives after mount useEffect(() => { setDraft(getNote(userId)); }, [getNote, userId]); const handleChange = (e: React.ChangeEvent) => { const val = e.target.value; setDraft(val); clearTimeout(saveTimer.current); saveTimer.current = setTimeout(async () => { setSaving(true); await setNote(userId, val); setSaving(false); }, 800); }; useEffect(() => () => clearTimeout(saveTimer.current), []); const charsLeft = USER_NOTE_MAX_LENGTH - draft.length; return ( Private Note {saving ? ( Saving… ) : ( {charsLeft < 100 ? `${charsLeft} left` : ''} )}