Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit

Permalink
Add face pile to rooms (#11356)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Germain authored Aug 30, 2023
1 parent af268b4 commit dc70ea5
Show file tree
Hide file tree
Showing 14 changed files with 447 additions and 133 deletions.
4 changes: 0 additions & 4 deletions res/css/structures/_SpaceRoomView.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,6 @@ limitations under the License.

.mx_FacePile {
display: inline-block;

.mx_FacePile_faces {
cursor: pointer;
}
}

.mx_SpaceRoomView_landing_inviteButton,
Expand Down
73 changes: 26 additions & 47 deletions res/css/views/elements/_FacePile.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -14,53 +14,32 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

.mx_FacePile {
display: flex;
align-items: center;

.mx_FacePile_faces {
display: inline-flex;
flex-direction: row-reverse;
vertical-align: middle;
margin: 0 -1px; /* to cancel out the border on the edges */

/* Overlap the children */
> * + * {
margin-right: -8px;
}

.mx_BaseAvatar {
border: 1px solid var(--facepile-background, $background);
}

.mx_FacePile_more {
position: relative;
border-radius: 100%;
width: 30px;
height: 30px;
background-color: $spacePanel-bg-color;

&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: inherit;
width: inherit;
background: $tertiary-content;
mask-position: center;
mask-size: 20px;
mask-repeat: no-repeat;
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
}
}
.mx_FacePile_more {
position: relative;
border-radius: 100%;
width: 30px;
height: 30px;
background-color: $spacePanel-bg-color;

&::before {
content: "";
z-index: 1;
position: absolute;
top: 0;
left: 0;
height: inherit;
width: inherit;
background: $tertiary-content;
mask-position: center;
mask-size: 20px;
mask-repeat: no-repeat;
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
}
}

.mx_FacePile_summary {
margin-left: 12px;
font: var(--cpd-font-body-md-regular);
line-height: $font-24px;
color: $tertiary-content;
}
.mx_FacePile_summary {
margin-left: 12px;
font: var(--cpd-font-body-md-regular);
line-height: $font-24px;
color: $tertiary-content;
}
17 changes: 17 additions & 0 deletions res/css/views/rooms/_RoomHeader.pcss
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ limitations under the License.

.mx_RoomHeader_info {
cursor: pointer;
flex: 1;
}

.mx_RoomHeader_topic {
Expand All @@ -45,3 +46,19 @@ limitations under the License.
height: calc($font-13px * 1.5);
opacity: 1;
}

.mx_RoomHeader .mx_FacePile {
color: $secondary-content;
display: flex;
align-items: center;
gap: var(--cpd-space-2x);
border-radius: 9999px;
padding: var(--cpd-space-1-5x);
cursor: pointer;
user-select: none;

&:hover {
color: $primary-content;
background: var(--cpd-color-bg-subtle-primary);
}
}
34 changes: 18 additions & 16 deletions src/components/views/elements/FacePile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>
),
);

Expand All @@ -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;
28 changes: 14 additions & 14 deletions src/components/views/elements/RoomFacePile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })}
Expand Down
50 changes: 44 additions & 6 deletions src/components/views/rooms/RoomHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -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");
Expand Down Expand Up @@ -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} />
Expand Down Expand Up @@ -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>
Expand All @@ -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>
);
}
33 changes: 18 additions & 15 deletions src/hooks/useAccountData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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)));

const handler = useCallback(
(event) => {
if (event.getType() !== eventType) return;
setValue(event.getContent());
},
[eventType],
);
useTypedEventEmitter(room, RoomEvent.AccountData, handler);

return value || ({} as T);
};
// Currently not used, commenting out otherwise the dead code CI is unhappy.
// But this code is valid and probably will be needed.

// export const useRoomAccountData = <T extends {}>(room: Room, eventType: string): T => {
// const [value, setValue] = useState<T | undefined>(() => tryGetContent<T>(room.getAccountData(eventType)));

// const handler = useCallback(
// (event) => {
// if (event.getType() !== eventType) return;
// setValue(event.getContent());
// },
// [eventType],
// );
// useTypedEventEmitter(room, RoomEvent.AccountData, handler);

// return value || ({} as T);
// };
9 changes: 4 additions & 5 deletions src/i18n/strings/en_EN.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion test/components/views/elements/FacePile-test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ describe("<FacePile />", () => {
const member = mkRoomMember("123", "456", "join");

const { asFragment } = render(
<FacePile members={[member]} size="36px" overflow={false} tooltip={<>tooltip</>} />,
<FacePile members={[member]} size="36px" overflow={false} tooltipLabel="tooltip" />,
);

expect(asFragment()).toMatchSnapshot();
Expand Down
Loading

0 comments on commit dc70ea5

Please sign in to comment.