Add face pile to rooms (#11356)
* Add face pile to rooms * Migrate FacePile to use Compound * Fix CI * Use FacePile component in room header * Add facepile tests * Make dead code CI happy * Lint * Fix tests * Fix CSS selectors * Update room face pile snapshot * Use useMemo instead of useState and useEffect * Remove unused imports * Update snapshot * Update snapshot
This commit is contained in:
parent
af268b4a03
commit
dc70ea5059
14 changed files with 445 additions and 131 deletions
|
@ -16,27 +16,27 @@ limitations under the License.
|
|||
|
||||
import React, { FC, HTMLAttributes, ReactNode } from "react";
|
||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||
import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
||||
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import TooltipTarget from "./TooltipTarget";
|
||||
import TextWithTooltip from "./TextWithTooltip";
|
||||
|
||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
members: RoomMember[];
|
||||
size: string;
|
||||
overflow: boolean;
|
||||
tooltip?: ReactNode;
|
||||
tooltipLabel?: string;
|
||||
tooltipShortcut?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
const FacePile: FC<IProps> = ({ members, size, overflow, tooltip, children, ...props }) => {
|
||||
const FacePile: FC<IProps> = ({ members, size, overflow, tooltipLabel, tooltipShortcut, children, ...props }) => {
|
||||
const faces = members.map(
|
||||
tooltip
|
||||
tooltipLabel
|
||||
? (m) => <MemberAvatar key={m.userId} member={m} size={size} hideTitle />
|
||||
: (m) => (
|
||||
<TooltipTarget key={m.userId} label={m.name}>
|
||||
<Tooltip key={m.userId} label={m.name} shortcut={tooltipShortcut}>
|
||||
<MemberAvatar member={m} size={size} viewUserOnClick={!props.onClick} hideTitle />
|
||||
</TooltipTarget>
|
||||
</Tooltip>
|
||||
),
|
||||
);
|
||||
|
||||
|
@ -47,18 +47,20 @@ const FacePile: FC<IProps> = ({ members, size, overflow, tooltip, children, ...p
|
|||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div {...props} className="mx_FacePile">
|
||||
{tooltip ? (
|
||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip}>
|
||||
{pileContents}
|
||||
</TextWithTooltip>
|
||||
) : (
|
||||
<div className="mx_FacePile_faces">{pileContents}</div>
|
||||
)}
|
||||
const content = (
|
||||
<div className="mx_FacePile">
|
||||
<AvatarStack>{pileContents}</AvatarStack>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
return tooltipLabel ? (
|
||||
<Tooltip label={tooltipLabel} shortcut={tooltipShortcut}>
|
||||
{content}
|
||||
</Tooltip>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
};
|
||||
|
||||
export default FacePile;
|
||||
|
|
|
@ -63,21 +63,21 @@ const RoomFacePile: FC<IProps> = ({ room, onlyKnownUsers = true, numShown = DEFA
|
|||
.reverse()
|
||||
.join(", ");
|
||||
|
||||
const tooltip = (
|
||||
<div>
|
||||
<div className="mx_Tooltip_title">
|
||||
{props.onClick ? _t("View all %(count)s members", { count }) : _t("%(count)s members", { count })}
|
||||
</div>
|
||||
<div className="mx_Tooltip_sub">
|
||||
{isJoined
|
||||
? _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers })
|
||||
: _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers })}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<FacePile members={shownMembers} size="28px" overflow={members.length > numShown} tooltip={tooltip} {...props}>
|
||||
<FacePile
|
||||
members={shownMembers}
|
||||
size="28px"
|
||||
overflow={members.length > numShown}
|
||||
tooltipLabel={
|
||||
props.onClick ? _t("View all %(count)s members", { count }) : _t("%(count)s members", { count })
|
||||
}
|
||||
tooltipShortcut={
|
||||
isJoined
|
||||
? _t("Including you, %(commaSeparatedMembers)s", { commaSeparatedMembers })
|
||||
: _t("Including %(commaSeparatedMembers)s", { commaSeparatedMembers })
|
||||
}
|
||||
{...props}
|
||||
>
|
||||
{onlyKnownUsers && (
|
||||
<span className="mx_FacePile_summary">
|
||||
{_t("%(count)s people you know have already joined", { count: members.length })}
|
||||
|
|
|
@ -21,14 +21,18 @@ import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/v
|
|||
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
|
||||
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { EventType } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useRoomName } from "../../../hooks/useRoomName";
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||
import { useTopic } from "../../../hooks/room/useTopic";
|
||||
import { useAccountData } from "../../../hooks/useAccountData";
|
||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||
import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||
import { _t, getCurrentLanguage } from "../../../languageHandler";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { Box } from "../../utils/Box";
|
||||
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
|
||||
|
@ -41,6 +45,7 @@ import { NotificationColor } from "../../../stores/notifications/NotificationCol
|
|||
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
||||
import SdkConfig from "../../../SdkConfig";
|
||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||
import FacePile from "../elements/FacePile";
|
||||
|
||||
/**
|
||||
* A helper to transform a notification color to the what the Compound Icon Button
|
||||
|
@ -67,9 +72,24 @@ function showOrHidePanel(phase: RightPanelPhases): void {
|
|||
}
|
||||
|
||||
export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||
const client = useMatrixClientContext();
|
||||
|
||||
const roomName = useRoomName(room);
|
||||
const roomTopic = useTopic(room);
|
||||
|
||||
const members = useRoomMembers(room);
|
||||
const memberCount = useRoomMemberCount(room);
|
||||
|
||||
const directRoomsList = useAccountData<Record<string, string[]>>(client, EventType.Direct);
|
||||
const isDirectMessage = useMemo(() => {
|
||||
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
|
||||
if (dmRoomList.includes(room?.roomId ?? "")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}, [directRoomsList, room?.roomId]);
|
||||
|
||||
const { voiceCallDisabledReason, voiceCallType, videoCallDisabledReason, videoCallType } = useRoomCallStatus(room);
|
||||
|
||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
|
@ -119,10 +139,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
gap="var(--cpd-space-3x)"
|
||||
className="mx_RoomHeader light-panel"
|
||||
onClick={() => {
|
||||
const rightPanel = RightPanelStore.instance;
|
||||
rightPanel.isOpen
|
||||
? rightPanel.togglePanel(null)
|
||||
: rightPanel.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||
}}
|
||||
>
|
||||
<DecoratedRoomAvatar room={room} size="40px" displayBadge={false} />
|
||||
|
@ -170,7 +187,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
onClick={() => {
|
||||
showOrHidePanel(RightPanelPhases.ThreadPanel);
|
||||
}}
|
||||
title={_t("Threads")}
|
||||
title={_t("common|threads")}
|
||||
>
|
||||
<ThreadsIcon />
|
||||
</IconButton>
|
||||
|
@ -184,6 +201,27 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
|||
<NotificationsIcon />
|
||||
</IconButton>
|
||||
</Flex>
|
||||
{!isDirectMessage && (
|
||||
<BodyText
|
||||
as="div"
|
||||
size="sm"
|
||||
weight="medium"
|
||||
aria-label={_t("%(count)s members", { count: memberCount })}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
showOrHidePanel(RightPanelPhases.RoomMemberList);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<FacePile
|
||||
className="mx_RoomHeader_members"
|
||||
members={members.slice(0, 3)}
|
||||
size="20px"
|
||||
overflow={false}
|
||||
>
|
||||
{memberCount.toLocaleString(getCurrentLanguage())}
|
||||
</FacePile>
|
||||
</BodyText>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { ClientEvent, MatrixClient, MatrixEvent, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { useTypedEventEmitter } from "./useEventEmitter";
|
||||
|
||||
|
@ -38,17 +38,20 @@ export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: strin
|
|||
};
|
||||
|
||||
// Hook to simplify listening to Matrix room account data
|
||||
export const useRoomAccountData = <T extends {}>(room: Room, eventType: string): T => {
|
||||
const [value, setValue] = useState<T | undefined>(() => tryGetContent<T>(room.getAccountData(eventType)));
|
||||
// Currently not used, commenting out otherwise the dead code CI is unhappy.
|
||||
// But this code is valid and probably will be needed.
|
||||
|
||||
const handler = useCallback(
|
||||
(event) => {
|
||||
if (event.getType() !== eventType) return;
|
||||
setValue(event.getContent());
|
||||
},
|
||||
[eventType],
|
||||
);
|
||||
useTypedEventEmitter(room, RoomEvent.AccountData, handler);
|
||||
// export const useRoomAccountData = <T extends {}>(room: Room, eventType: string): T => {
|
||||
// const [value, setValue] = useState<T | undefined>(() => tryGetContent<T>(room.getAccountData(eventType)));
|
||||
|
||||
return value || ({} as T);
|
||||
};
|
||||
// const handler = useCallback(
|
||||
// (event) => {
|
||||
// if (event.getType() !== eventType) return;
|
||||
// setValue(event.getContent());
|
||||
// },
|
||||
// [eventType],
|
||||
// );
|
||||
// useTypedEventEmitter(room, RoomEvent.AccountData, handler);
|
||||
|
||||
// return value || ({} as T);
|
||||
// };
|
||||
|
|
|
@ -1821,16 +1821,15 @@
|
|||
"Room %(name)s": "Room %(name)s",
|
||||
"Recently visited rooms": "Recently visited rooms",
|
||||
"No recently visited rooms": "No recently visited rooms",
|
||||
"Threads": "Threads",
|
||||
"%(count)s members": {
|
||||
"other": "%(count)s members",
|
||||
"one": "%(count)s member"
|
||||
},
|
||||
"Video room": "Video room",
|
||||
"Public space": "Public space",
|
||||
"Public room": "Public room",
|
||||
"Private space": "Private space",
|
||||
"Private room": "Private room",
|
||||
"%(count)s members": {
|
||||
"other": "%(count)s members",
|
||||
"one": "%(count)s member"
|
||||
},
|
||||
"Start new chat": "Start new chat",
|
||||
"Invite to space": "Invite to space",
|
||||
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue