fix: follow mode collaborator status indicator (#7459)

This commit is contained in:
David Luzar 2023-12-18 16:14:25 +01:00 committed by GitHub
parent 2a0fe2584e
commit 0808532b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 75 additions and 34 deletions

View File

@ -1,5 +1,8 @@
import { getClientColor } from "../clients";
import { Avatar } from "../components/Avatar";
import { GoToCollaboratorComponentProps } from "../components/UserList";
import { eyeIcon } from "../components/icons";
import { t } from "../i18n";
import { Collaborator } from "../types";
import { register } from "./register";
@ -35,38 +38,43 @@ export const actionGoToCollaborator = register({
};
},
PanelComponent: ({ updateData, data, appState }) => {
const [clientId, collaborator, withName] = data as [
string,
Collaborator,
boolean,
];
const [socketId, collaborator, withName, isBeingFollowed] =
data as GoToCollaboratorComponentProps;
const background = getClientColor(clientId);
const background = getClientColor(socketId);
return withName ? (
<div
className="dropdown-menu-item dropdown-menu-item-base"
onClick={() => updateData({ ...collaborator, clientId })}
className="dropdown-menu-item dropdown-menu-item-base UserList__collaborator"
onClick={() => updateData({ ...collaborator, socketId })}
>
<Avatar
color={background}
onClick={() => {}}
name={collaborator.username || ""}
src={collaborator.avatarUrl}
isBeingFollowed={appState.userToFollow?.socketId === clientId}
isBeingFollowed={isBeingFollowed}
isCurrentUser={collaborator.isCurrentUser === true}
/>
{collaborator.username}
<div
className="UserList__collaborator-follow-status-icon"
style={{ visibility: isBeingFollowed ? "visible" : "hidden" }}
title={isBeingFollowed ? t("userList.hint.followStatus") : undefined}
aria-hidden
>
{eyeIcon}
</div>
</div>
) : (
<Avatar
color={background}
onClick={() => {
updateData({ ...collaborator, clientId });
updateData({ ...collaborator, socketId });
}}
name={collaborator.username || ""}
src={collaborator.avatarUrl}
isBeingFollowed={appState.userToFollow?.socketId === clientId}
isBeingFollowed={isBeingFollowed}
isCurrentUser={collaborator.isCurrentUser === true}
/>
);

View File

@ -3472,6 +3472,7 @@ class App extends React.Component<AppProps, AppState> {
};
private maybeUnfollowRemoteUser = () => {
console.warn("maybeUnfollowRemoteUser");
if (this.state.userToFollow) {
this.setState({ userToFollow: null });
}

View File

@ -339,7 +339,10 @@ const LayerUI = ({
)}
>
{appState.collaborators.size > 0 && (
<UserList collaborators={appState.collaborators} />
<UserList
collaborators={appState.collaborators}
userToFollow={appState.userToFollow?.socketId || null}
/>
)}
{renderTopRightUI?.(device.editor.isMobile, appState)}
{!appState.viewModeEnabled &&

View File

@ -80,6 +80,7 @@ type TooltipProps = {
label: string;
long?: boolean;
style?: React.CSSProperties;
disabled?: boolean;
};
export const Tooltip = ({
@ -87,11 +88,15 @@ export const Tooltip = ({
label,
long = false,
style,
disabled,
}: TooltipProps) => {
useEffect(() => {
return () =>
getTooltipDiv().classList.remove("excalidraw-tooltip--visible");
}, []);
if (disabled) {
return null;
}
return (
<div
className="excalidraw-tooltip-wrapper"

View File

@ -51,6 +51,13 @@
color: var(--color-gray-100);
}
.UserList__collaborator-follow-status-icon {
margin-left: auto;
flex: 0 0 auto;
width: 1rem;
display: flex;
}
--userlist-hint-bg-color: var(--color-gray-10);
--userlist-hint-heading-color: var(--color-gray-80);
--userlist-hint-text-color: var(--color-gray-60);

View File

@ -13,51 +13,62 @@ import { searchIcon } from "./icons";
import { t } from "../i18n";
import { isShallowEqual } from "../utils";
export type GoToCollaboratorComponentProps = [
SocketId,
Collaborator,
boolean,
boolean,
];
const FIRST_N_AVATARS = 3;
const SHOW_COLLABORATORS_FILTER_AT = 8;
const ConditionalTooltipWrapper = ({
shouldWrap,
children,
clientId,
socketId,
username,
}: {
shouldWrap: boolean;
children: React.ReactNode;
username?: string | null;
clientId: string;
socketId: string;
}) =>
shouldWrap ? (
<Tooltip label={username || "Unknown user"} key={clientId}>
<Tooltip label={username || "Unknown user"} key={socketId}>
{children}
</Tooltip>
) : (
<React.Fragment key={clientId}>{children}</React.Fragment>
<React.Fragment key={socketId}>{children}</React.Fragment>
);
const renderCollaborator = ({
actionManager,
collaborator,
clientId,
socketId,
withName = false,
shouldWrapWithTooltip = false,
isBeingFollowed,
}: {
actionManager: ActionManager;
collaborator: Collaborator;
clientId: string;
socketId: string;
withName?: boolean;
shouldWrapWithTooltip?: boolean;
isBeingFollowed: boolean;
}) => {
const avatarJSX = actionManager.renderAction("goToCollaborator", [
clientId,
const data: GoToCollaboratorComponentProps = [
socketId,
collaborator,
withName,
]);
isBeingFollowed,
];
const avatarJSX = actionManager.renderAction("goToCollaborator", data);
return (
<ConditionalTooltipWrapper
key={clientId}
clientId={clientId}
key={socketId}
socketId={socketId}
username={collaborator.username}
shouldWrap={shouldWrapWithTooltip}
>
@ -75,6 +86,7 @@ type UserListProps = {
className?: string;
mobile?: boolean;
collaborators: Map<SocketId, UserListUserObject>;
userToFollow: SocketId | null;
};
const collaboratorComparatorKeys = [
@ -85,7 +97,7 @@ const collaboratorComparatorKeys = [
] as const;
export const UserList = React.memo(
({ className, mobile, collaborators }: UserListProps) => {
({ className, mobile, collaborators, userToFollow }: UserListProps) => {
const actionManager = useExcalidrawActionManager();
const uniqueCollaboratorsMap = new Map<string, Collaborator>();
@ -98,7 +110,6 @@ export const UserList = React.memo(
);
});
// const uniqueCollaboratorsMap = sampleCollaborators;
const uniqueCollaboratorsArray = Array.from(uniqueCollaboratorsMap).filter(
([_, collaborator]) => collaborator.username?.trim(),
);
@ -123,23 +134,25 @@ export const UserList = React.memo(
);
const firstNAvatarsJSX = firstNCollaborators.map(
([clientId, collaborator]) =>
([socketId, collaborator]) =>
renderCollaborator({
actionManager,
collaborator,
clientId,
socketId,
shouldWrapWithTooltip: true,
isBeingFollowed: socketId === userToFollow,
}),
);
return mobile ? (
<div className={clsx("UserList UserList_mobile", className)}>
{uniqueCollaboratorsArray.map(([clientId, collaborator]) =>
{uniqueCollaboratorsArray.map(([socketId, collaborator]) =>
renderCollaborator({
actionManager,
collaborator,
clientId,
socketId,
shouldWrapWithTooltip: true,
isBeingFollowed: socketId === userToFollow,
}),
)}
</div>
@ -161,7 +174,7 @@ export const UserList = React.memo(
<Popover.Content
style={{
zIndex: 2,
width: "12rem",
width: "13rem",
textAlign: "left",
}}
align="end"
@ -192,12 +205,13 @@ export const UserList = React.memo(
<div className="UserList__hint">
{t("userList.hint.text")}
</div>
{filteredCollaborators.map(([clientId, collaborator]) =>
{filteredCollaborators.map(([socketId, collaborator]) =>
renderCollaborator({
actionManager,
collaborator,
clientId,
socketId,
withName: true,
isBeingFollowed: socketId === userToFollow,
}),
)}
</div>
@ -212,7 +226,8 @@ export const UserList = React.memo(
if (
prev.collaborators.size !== next.collaborators.size ||
prev.mobile !== next.mobile ||
prev.className !== next.className
prev.className !== next.className ||
prev.userToFollow !== next.userToFollow
) {
return false;
}

View File

@ -60,6 +60,7 @@ const MainMenu = Object.assign(
<UserList
mobile={true}
collaborators={appState.collaborators}
userToFollow={appState.userToFollow?.socketId || null}
/>
</fieldset>
)}

View File

@ -528,7 +528,8 @@
"empty": "No users found"
},
"hint": {
"text": "Click on user to follow"
"text": "Click on user to follow",
"followStatus": "You're currently following this user"
}
}
}