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 { getClientColor } from "../clients";
import { Avatar } from "../components/Avatar"; import { Avatar } from "../components/Avatar";
import { GoToCollaboratorComponentProps } from "../components/UserList";
import { eyeIcon } from "../components/icons";
import { t } from "../i18n";
import { Collaborator } from "../types"; import { Collaborator } from "../types";
import { register } from "./register"; import { register } from "./register";
@ -35,38 +38,43 @@ export const actionGoToCollaborator = register({
}; };
}, },
PanelComponent: ({ updateData, data, appState }) => { PanelComponent: ({ updateData, data, appState }) => {
const [clientId, collaborator, withName] = data as [ const [socketId, collaborator, withName, isBeingFollowed] =
string, data as GoToCollaboratorComponentProps;
Collaborator,
boolean,
];
const background = getClientColor(clientId); const background = getClientColor(socketId);
return withName ? ( return withName ? (
<div <div
className="dropdown-menu-item dropdown-menu-item-base" className="dropdown-menu-item dropdown-menu-item-base UserList__collaborator"
onClick={() => updateData({ ...collaborator, clientId })} onClick={() => updateData({ ...collaborator, socketId })}
> >
<Avatar <Avatar
color={background} color={background}
onClick={() => {}} onClick={() => {}}
name={collaborator.username || ""} name={collaborator.username || ""}
src={collaborator.avatarUrl} src={collaborator.avatarUrl}
isBeingFollowed={appState.userToFollow?.socketId === clientId} isBeingFollowed={isBeingFollowed}
isCurrentUser={collaborator.isCurrentUser === true} isCurrentUser={collaborator.isCurrentUser === true}
/> />
{collaborator.username} {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> </div>
) : ( ) : (
<Avatar <Avatar
color={background} color={background}
onClick={() => { onClick={() => {
updateData({ ...collaborator, clientId }); updateData({ ...collaborator, socketId });
}} }}
name={collaborator.username || ""} name={collaborator.username || ""}
src={collaborator.avatarUrl} src={collaborator.avatarUrl}
isBeingFollowed={appState.userToFollow?.socketId === clientId} isBeingFollowed={isBeingFollowed}
isCurrentUser={collaborator.isCurrentUser === true} isCurrentUser={collaborator.isCurrentUser === true}
/> />
); );

View File

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

View File

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

View File

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

View File

@ -51,6 +51,13 @@
color: var(--color-gray-100); 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-bg-color: var(--color-gray-10);
--userlist-hint-heading-color: var(--color-gray-80); --userlist-hint-heading-color: var(--color-gray-80);
--userlist-hint-text-color: var(--color-gray-60); --userlist-hint-text-color: var(--color-gray-60);

View File

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

View File

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

View File

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