import React, { ChangeEventHandler, MouseEventHandler, useCallback, useMemo, useRef, useState, } from 'react'; import { Avatar, Badge, Box, Chip, Header, Icon, IconButton, Icons, Input, MenuItem, PopOut, RectCords, Scroll, Spinner, Text, Tooltip, TooltipProvider, config, } from 'folds'; import { Room, RoomMember } from 'matrix-js-sdk'; import { useVirtualizer } from '@tanstack/react-virtual'; import classNames from 'classnames'; import * as css from './MembersDrawer.css'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { UseStateProvider } from '../../components/UseStateProvider'; import { SearchItemStrGetter, UseAsyncSearchOptions, useAsyncSearch, } from '../../hooks/useAsyncSearch'; import { useDebounce } from '../../hooks/useDebounce'; import { usePowerLevelTags, useFlattenPowerLevelTagMembers } from '../../hooks/usePowerLevelTags'; import { TypingIndicator } from '../../components/typing-indicator'; import { getMemberDisplayName, getMemberSearchStr } from '../../utils/room'; import { getMxIdLocalPart } from '../../utils/matrix'; import { useSetSetting, useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { millify } from '../../plugins/millify'; import { ScrollTopContainer } from '../../components/scroll-top-container'; import { UserAvatar } from '../../components/user-avatar'; import { useRoomTypingMember } from '../../hooks/useRoomTypingMembers'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useMembershipFilter, useMembershipFilterMenu } from '../../hooks/useMemberFilter'; import { useMemberSort, useMemberSortMenu } from '../../hooks/useMemberSort'; import { usePowerLevelsAPI, usePowerLevelsContext } from '../../hooks/usePowerLevels'; import { MembershipFilterMenu } from '../../components/MembershipFilterMenu'; import { MemberSortMenu } from '../../components/MemberSortMenu'; import { useOpenUserRoomProfile, useUserRoomProfileState } from '../../state/hooks/userRoomProfile'; import { useSpaceOptionally } from '../../hooks/useSpace'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, matchOptions: { contain: true, }, }; const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId; const getRoomMemberStr: SearchItemStrGetter = (m, query) => getMemberSearchStr(m, query, mxIdToName); type MembersDrawerProps = { room: Room; members: RoomMember[]; }; export function MembersDrawer({ room, members }: MembersDrawerProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const scrollRef = useRef(null); const searchInputRef = useRef(null); const scrollTopAnchorRef = useRef(null); const powerLevels = usePowerLevelsContext(); const [, getPowerLevelTag] = usePowerLevelTags(room, powerLevels); const fetchingMembers = members.length < room.getJoinedMemberCount(); const setPeopleDrawer = useSetSetting(settingsAtom, 'isPeopleDrawer'); const openUserRoomProfile = useOpenUserRoomProfile(); const space = useSpaceOptionally(); const openProfileUserId = useUserRoomProfileState()?.userId; const membershipFilterMenu = useMembershipFilterMenu(); const sortFilterMenu = useMemberSortMenu(); const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex'); const [membershipFilterIndex, setMembershipFilterIndex] = useState(0); const { getPowerLevel } = usePowerLevelsAPI(powerLevels); const membershipFilter = useMembershipFilter(membershipFilterIndex, membershipFilterMenu); const memberSort = useMemberSort(sortFilterIndex, sortFilterMenu); const typingMembers = useRoomTypingMember(room.roomId); const filteredMembers = useMemo( () => members .filter(membershipFilter.filterFn) .sort(memberSort.sortFn) .sort((a, b) => b.powerLevel - a.powerLevel), [members, membershipFilter, memberSort] ); const [result, search, resetSearch] = useAsyncSearch( filteredMembers, getRoomMemberStr, SEARCH_OPTIONS ); if (!result && searchInputRef.current?.value) search(searchInputRef.current.value); const processMembers = result ? result.items : filteredMembers; const PLTagOrRoomMember = useFlattenPowerLevelTagMembers( processMembers, getPowerLevel, getPowerLevelTag ); const virtualizer = useVirtualizer({ count: PLTagOrRoomMember.length, getScrollElement: () => scrollRef.current, estimateSize: () => 40, overscan: 10, }); const handleSearchChange: ChangeEventHandler = useDebounce( useCallback( (evt) => { if (evt.target.value) search(evt.target.value); else resetSearch(); }, [search, resetSearch] ), { wait: 200 } ); const getName = (member: RoomMember) => getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId; const handleMemberClick: MouseEventHandler = (evt) => { const btn = evt.currentTarget as HTMLButtonElement; const userId = btn.getAttribute('data-user-id'); if (!userId) return; openUserRoomProfile(room.roomId, space?.roomId, userId, btn.getBoundingClientRect(), 'Left'); }; return (
{`${millify(room.getJoinedMemberCount())} Members`} Close } > {(triggerRef) => ( setPeopleDrawer(false)} > )}
{(anchor: RectCords | undefined, setAnchor) => ( setAnchor(undefined)} /> } > setAnchor( evt.currentTarget.getBoundingClientRect() )) as MouseEventHandler } variant="Background" size="400" radii="300" before={} > {membershipFilter.name} )} {(anchor: RectCords | undefined, setAnchor) => ( setAnchor(undefined)} /> } > setAnchor( evt.currentTarget.getBoundingClientRect() )) as MouseEventHandler } variant="Background" size="400" radii="300" after={} > {memberSort.name} )} } after={ result && ( 0 ? 'Success' : 'Critical'} size="400" radii="Pill" aria-pressed onClick={() => { if (searchInputRef.current) { searchInputRef.current.value = ''; searchInputRef.current.focus(); } resetSearch(); }} after={} > {`${result.items.length || 'No'} ${ result.items.length === 1 ? 'Result' : 'Results' }`} ) } /> virtualizer.scrollToOffset(0)} variant="Surface" radii="Pill" outlined size="300" aria-label="Scroll to Top" > {!fetchingMembers && !result && processMembers.length === 0 && ( {`No "${membershipFilter.name}" Members`} )}
{virtualizer.getVirtualItems().map((vItem) => { const tagOrMember = PLTagOrRoomMember[vItem.index]; if (!('userId' in tagOrMember)) { return ( {tagOrMember.name} ); } const member = tagOrMember; const name = getName(member); const avatarMxcUrl = member.getMxcAvatarUrl(); const avatarUrl = avatarMxcUrl ? mx.mxcUrlToHttp( avatarMxcUrl, 100, 100, 'crop', undefined, false, useAuthentication ) : undefined; return ( } /> } after={ typingMembers.find((receipt) => receipt.userId === member.userId) && ( ) } > {name} ); })}
{fetchingMembers && ( )}
); }