import React, { ChangeEventHandler, MouseEventHandler, useCallback, useMemo, useRef, useState, } from 'react'; import { Box, Chip, config, Icon, IconButton, Icons, Input, PopOut, RectCords, Scroll, Spinner, Text, toRem, } from 'folds'; import { useVirtualizer } from '@tanstack/react-virtual'; import { RoomMember } from 'matrix-js-sdk'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { useRoom } from '../../../hooks/useRoom'; import { useRoomMembers } from '../../../hooks/useRoomMembers'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { useGetMemberPowerLevel, usePowerLevels } from '../../../hooks/usePowerLevels'; import { VirtualTile } from '../../../components/virtualizer'; import { MemberTile } from '../../../components/member-tile'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; import { getMxIdLocalPart, getMxIdServer } from '../../../utils/matrix'; import { ServerBadge } from '../../../components/server-badge'; import { useDebounce } from '../../../hooks/useDebounce'; import { SearchItemStrGetter, useAsyncSearch, UseAsyncSearchOptions, } from '../../../hooks/useAsyncSearch'; import { getMemberSearchStr } from '../../../utils/room'; import { useMembershipFilter, useMembershipFilterMenu } from '../../../hooks/useMemberFilter'; import { useMemberPowerSort, useMemberSort, useMemberSortMenu } from '../../../hooks/useMemberSort'; import { settingsAtom } from '../../../state/settings'; import { useSetting } from '../../../state/hooks/settings'; import { UseStateProvider } from '../../../components/UseStateProvider'; import { MembershipFilterMenu } from '../../../components/MembershipFilterMenu'; import { MemberSortMenu } from '../../../components/MemberSortMenu'; import { ScrollTopContainer } from '../../../components/scroll-top-container'; import { useOpenUserRoomProfile, useUserRoomProfileState, } from '../../../state/hooks/userRoomProfile'; import { useSpaceOptionally } from '../../../hooks/useSpace'; import { useFlattenPowerTagMembers, useGetMemberPowerTag } from '../../../hooks/useMemberPowerTag'; import { useRoomCreators } from '../../../hooks/useRoomCreators'; import { getMouseEventCords } from '../../../utils/dom'; const SEARCH_OPTIONS: UseAsyncSearchOptions = { limit: 1000, matchOptions: { contain: true, }, normalizeOptions: { ignoreWhitespace: false, }, }; const mxIdToName = (mxId: string) => getMxIdLocalPart(mxId) ?? mxId; const getRoomMemberStr: SearchItemStrGetter = (m, query) => getMemberSearchStr(m, query, mxIdToName); type MembersProps = { requestClose: () => void; }; export function Members({ requestClose }: MembersProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const room = useRoom(); const members = useRoomMembers(mx, room.roomId); const fetchingMembers = members.length < room.getJoinedMemberCount(); const openProfile = useOpenUserRoomProfile(); const profileUser = useUserRoomProfileState(); const space = useSpaceOptionally(); const powerLevels = usePowerLevels(room); const creators = useRoomCreators(room); const getPowerTag = useGetMemberPowerTag(room, creators, powerLevels); const getPowerLevel = useGetMemberPowerLevel(powerLevels); const [membershipFilterIndex, setMembershipFilterIndex] = useState(0); const [sortFilterIndex, setSortFilterIndex] = useSetting(settingsAtom, 'memberSortFilterIndex'); const membershipFilter = useMembershipFilter(membershipFilterIndex, useMembershipFilterMenu()); const memberSort = useMemberSort(sortFilterIndex, useMemberSortMenu()); const memberPowerSort = useMemberPowerSort(creators, getPowerLevel); const scrollRef = useRef(null); const searchInputRef = useRef(null); const scrollTopAnchorRef = useRef(null); const sortedMembers = useMemo( () => Array.from(members) .filter(membershipFilter.filterFn) .sort(memberSort.sortFn) .sort(memberPowerSort), [members, membershipFilter, memberSort, memberPowerSort] ); const [result, search, resetSearch] = useAsyncSearch( sortedMembers, getRoomMemberStr, SEARCH_OPTIONS ); if (!result && searchInputRef.current?.value) search(searchInputRef.current.value); const flattenTagMembers = useFlattenPowerTagMembers(result?.items ?? sortedMembers, getPowerTag); const virtualizer = useVirtualizer({ count: flattenTagMembers.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 handleSearchReset = () => { if (searchInputRef.current) { searchInputRef.current.value = ''; searchInputRef.current.focus(); } resetSearch(); }; const handleMemberClick: MouseEventHandler = (evt) => { const btn = evt.currentTarget as HTMLButtonElement; const userId = btn.getAttribute('data-user-id'); if (userId) { openProfile(room.roomId, space?.roomId, userId, getMouseEventCords(evt.nativeEvent)); } }; return ( {room.getJoinedMemberCount()} Members } variant="SurfaceVariant" size="500" placeholder="Search" outlined after={ result && ( 0 ? 'Success' : 'Critical'} outlined size="400" radii="Pill" aria-pressed onClick={handleSearchReset} after={} > {result.items.length === 0 ? 'No Results' : `${result.items.length} Results`} ) } /> {(anchor: RectCords | undefined, setAnchor) => ( setAnchor(undefined)} /> } > setAnchor( evt.currentTarget.getBoundingClientRect() )) as MouseEventHandler } variant="SurfaceVariant" size="400" radii="300" before={} > {membershipFilter.name} )} {(anchor: RectCords | undefined, setAnchor) => ( setAnchor(undefined)} /> } > setAnchor( evt.currentTarget.getBoundingClientRect() )) as MouseEventHandler } variant="SurfaceVariant" size="400" radii="300" after={} > {memberSort.name} )} virtualizer.scrollToOffset(0)} variant="Surface" radii="Pill" outlined size="300" aria-label="Scroll to Top" > {fetchingMembers && ( )} {!fetchingMembers && !result && flattenTagMembers.length === 0 && ( {`No "${membershipFilter.name}" Members`} )} {virtualizer.getVirtualItems().map((vItem) => { const tagOrMember = flattenTagMembers[vItem.index]; if ('userId' in tagOrMember) { const server = getMxIdServer(tagOrMember.userId); return (
) } />
); } return (
{tagOrMember.name}
); })}
); }