import "./UserList.scss"; import React from "react"; import clsx from "clsx"; import { Collaborator, SocketId } from "../types"; import { Tooltip } from "./Tooltip"; import { useExcalidrawActionManager } from "./App"; import { ActionManager } from "../actions/manager"; import * as Popover from "@radix-ui/react-popover"; import { Island } from "./Island"; import { searchIcon } from "./icons"; import { t } from "../i18n"; import { isShallowEqual } from "../utils"; export type GoToCollaboratorComponentProps = { clientId: ClientId; collaborator: Collaborator; withName: boolean; isBeingFollowed: boolean; }; /** collaborator user id or socket id (fallback) */ type ClientId = string & { _brand: "UserId" }; const FIRST_N_AVATARS = 3; const SHOW_COLLABORATORS_FILTER_AT = 8; const ConditionalTooltipWrapper = ({ shouldWrap, children, clientId, username, }: { shouldWrap: boolean; children: React.ReactNode; username?: string | null; clientId: ClientId; }) => shouldWrap ? ( {children} ) : ( {children} ); const renderCollaborator = ({ actionManager, collaborator, clientId, withName = false, shouldWrapWithTooltip = false, isBeingFollowed, }: { actionManager: ActionManager; collaborator: Collaborator; clientId: ClientId; withName?: boolean; shouldWrapWithTooltip?: boolean; isBeingFollowed: boolean; }) => { const data: GoToCollaboratorComponentProps = { clientId, collaborator, withName, isBeingFollowed, }; const avatarJSX = actionManager.renderAction("goToCollaborator", data); return ( {avatarJSX} ); }; type UserListUserObject = Pick< Collaborator, "avatarUrl" | "id" | "socketId" | "username" >; type UserListProps = { className?: string; mobile?: boolean; collaborators: Map; userToFollow: SocketId | null; }; const collaboratorComparatorKeys = [ "avatarUrl", "id", "socketId", "username", ] as const; export const UserList = React.memo( ({ className, mobile, collaborators, userToFollow }: UserListProps) => { const actionManager = useExcalidrawActionManager(); const uniqueCollaboratorsMap = new Map(); collaborators.forEach((collaborator, socketId) => { const userId = (collaborator.id || socketId) as ClientId; uniqueCollaboratorsMap.set( // filter on user id, else fall back on unique socketId userId, { ...collaborator, socketId }, ); }); const uniqueCollaboratorsArray = Array.from(uniqueCollaboratorsMap).filter( ([_, collaborator]) => collaborator.username?.trim(), ); const [searchTerm, setSearchTerm] = React.useState(""); if (uniqueCollaboratorsArray.length === 0) { return null; } const searchTermNormalized = searchTerm.trim().toLowerCase(); const filteredCollaborators = searchTermNormalized ? uniqueCollaboratorsArray.filter(([, collaborator]) => collaborator.username?.toLowerCase().includes(searchTerm), ) : uniqueCollaboratorsArray; const firstNCollaborators = uniqueCollaboratorsArray.slice( 0, FIRST_N_AVATARS, ); const firstNAvatarsJSX = firstNCollaborators.map( ([clientId, collaborator]) => renderCollaborator({ actionManager, collaborator, clientId, shouldWrapWithTooltip: true, isBeingFollowed: collaborator.socketId === userToFollow, }), ); return mobile ? (
{uniqueCollaboratorsArray.map(([clientId, collaborator]) => renderCollaborator({ actionManager, collaborator, clientId, shouldWrapWithTooltip: true, isBeingFollowed: collaborator.socketId === userToFollow, }), )}
) : (
{firstNAvatarsJSX} {uniqueCollaboratorsArray.length > FIRST_N_AVATARS && ( { if (!isOpen) { setSearchTerm(""); } }} > +{uniqueCollaboratorsArray.length - FIRST_N_AVATARS} {uniqueCollaboratorsArray.length >= SHOW_COLLABORATORS_FILTER_AT && (
{searchIcon} { setSearchTerm(e.target.value); }} />
)}
{filteredCollaborators.length === 0 && (
{t("userList.search.empty")}
)}
{t("userList.hint.text")}
{filteredCollaborators.map(([clientId, collaborator]) => renderCollaborator({ actionManager, collaborator, clientId, withName: true, isBeingFollowed: collaborator.socketId === userToFollow, }), )}
)}
); }, (prev, next) => { if ( prev.collaborators.size !== next.collaborators.size || prev.mobile !== next.mobile || prev.className !== next.className || prev.userToFollow !== next.userToFollow ) { return false; } for (const [socketId, collaborator] of prev.collaborators) { const nextCollaborator = next.collaborators.get(socketId); if ( !nextCollaborator || !isShallowEqual( collaborator, nextCollaborator, collaboratorComparatorKeys, ) ) { return false; } } return true; }, );