fix: follow mode collaborator status indicator (#7459)
This commit is contained in:
parent
2a0fe2584e
commit
0808532b49
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
|
@ -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 &&
|
||||||
|
@ -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"
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user