mirror of https://github.com/vector-im/riot-web
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 snapshotpull/28788/head^2
parent
af268b4a03
commit
dc70ea5059
|
@ -189,10 +189,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_FacePile {
|
.mx_FacePile {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
.mx_FacePile_faces {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceRoomView_landing_inviteButton,
|
.mx_SpaceRoomView_landing_inviteButton,
|
||||||
|
|
|
@ -14,25 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 {
|
.mx_FacePile_more {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
|
@ -55,7 +36,6 @@ limitations under the License.
|
||||||
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
|
mask-image: url("$(res)/img/element-icons/room/ellipsis.svg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_FacePile_summary {
|
.mx_FacePile_summary {
|
||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
|
@ -63,4 +43,3 @@ limitations under the License.
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
color: $tertiary-content;
|
color: $tertiary-content;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_RoomHeader_info {
|
.mx_RoomHeader_info {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_topic {
|
.mx_RoomHeader_topic {
|
||||||
|
@ -45,3 +46,19 @@ limitations under the License.
|
||||||
height: calc($font-13px * 1.5);
|
height: calc($font-13px * 1.5);
|
||||||
opacity: 1;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,27 +16,27 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { FC, HTMLAttributes, ReactNode } from "react";
|
import React, { FC, HTMLAttributes, ReactNode } from "react";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { AvatarStack, Tooltip } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
import TooltipTarget from "./TooltipTarget";
|
|
||||||
import TextWithTooltip from "./TextWithTooltip";
|
|
||||||
|
|
||||||
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
interface IProps extends HTMLAttributes<HTMLSpanElement> {
|
||||||
members: RoomMember[];
|
members: RoomMember[];
|
||||||
size: string;
|
size: string;
|
||||||
overflow: boolean;
|
overflow: boolean;
|
||||||
tooltip?: ReactNode;
|
tooltipLabel?: string;
|
||||||
|
tooltipShortcut?: string;
|
||||||
children?: ReactNode;
|
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(
|
const faces = members.map(
|
||||||
tooltip
|
tooltipLabel
|
||||||
? (m) => <MemberAvatar key={m.userId} member={m} size={size} hideTitle />
|
? (m) => <MemberAvatar key={m.userId} member={m} size={size} hideTitle />
|
||||||
: (m) => (
|
: (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 />
|
<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 (
|
const content = (
|
||||||
<div {...props} className="mx_FacePile">
|
<div className="mx_FacePile">
|
||||||
{tooltip ? (
|
<AvatarStack>{pileContents}</AvatarStack>
|
||||||
<TextWithTooltip class="mx_FacePile_faces" tooltip={tooltip}>
|
|
||||||
{pileContents}
|
|
||||||
</TextWithTooltip>
|
|
||||||
) : (
|
|
||||||
<div className="mx_FacePile_faces">{pileContents}</div>
|
|
||||||
)}
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return tooltipLabel ? (
|
||||||
|
<Tooltip label={tooltipLabel} shortcut={tooltipShortcut}>
|
||||||
|
{content}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FacePile;
|
export default FacePile;
|
||||||
|
|
|
@ -63,21 +63,21 @@ const RoomFacePile: FC<IProps> = ({ room, onlyKnownUsers = true, numShown = DEFA
|
||||||
.reverse()
|
.reverse()
|
||||||
.join(", ");
|
.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 (
|
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 && (
|
{onlyKnownUsers && (
|
||||||
<span className="mx_FacePile_summary">
|
<span className="mx_FacePile_summary">
|
||||||
{_t("%(count)s people you know have already joined", { count: members.length })}
|
{_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 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 { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
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 type { Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { useRoomName } from "../../../hooks/useRoomName";
|
import { useRoomName } from "../../../hooks/useRoomName";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
import { useTopic } from "../../../hooks/room/useTopic";
|
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 { Flex } from "../../utils/Flex";
|
||||||
import { Box } from "../../utils/Box";
|
import { Box } from "../../utils/Box";
|
||||||
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
|
import { useRoomCallStatus } from "../../../hooks/room/useRoomCallStatus";
|
||||||
|
@ -41,6 +45,7 @@ import { NotificationColor } from "../../../stores/notifications/NotificationCol
|
||||||
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
import { useGlobalNotificationState } from "../../../hooks/useGlobalNotificationState";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
import { useFeatureEnabled } from "../../../hooks/useSettings";
|
||||||
|
import FacePile from "../elements/FacePile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A helper to transform a notification color to the what the Compound Icon Button
|
* 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 {
|
export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
const roomName = useRoomName(room);
|
const roomName = useRoomName(room);
|
||||||
const roomTopic = useTopic(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 { voiceCallDisabledReason, voiceCallType, videoCallDisabledReason, videoCallType } = useRoomCallStatus(room);
|
||||||
|
|
||||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||||
|
@ -119,10 +139,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
gap="var(--cpd-space-3x)"
|
gap="var(--cpd-space-3x)"
|
||||||
className="mx_RoomHeader light-panel"
|
className="mx_RoomHeader light-panel"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const rightPanel = RightPanelStore.instance;
|
showOrHidePanel(RightPanelPhases.RoomSummary);
|
||||||
rightPanel.isOpen
|
|
||||||
? rightPanel.togglePanel(null)
|
|
||||||
: rightPanel.setCard({ phase: RightPanelPhases.RoomSummary });
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DecoratedRoomAvatar room={room} size="40px" displayBadge={false} />
|
<DecoratedRoomAvatar room={room} size="40px" displayBadge={false} />
|
||||||
|
@ -170,7 +187,7 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showOrHidePanel(RightPanelPhases.ThreadPanel);
|
showOrHidePanel(RightPanelPhases.ThreadPanel);
|
||||||
}}
|
}}
|
||||||
title={_t("Threads")}
|
title={_t("common|threads")}
|
||||||
>
|
>
|
||||||
<ThreadsIcon />
|
<ThreadsIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -184,6 +201,27 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
<NotificationsIcon />
|
<NotificationsIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Flex>
|
</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>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useCallback, useState } from "react";
|
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";
|
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
|
// Hook to simplify listening to Matrix room account data
|
||||||
export const useRoomAccountData = <T extends {}>(room: Room, eventType: string): T => {
|
// Currently not used, commenting out otherwise the dead code CI is unhappy.
|
||||||
const [value, setValue] = useState<T | undefined>(() => tryGetContent<T>(room.getAccountData(eventType)));
|
// But this code is valid and probably will be needed.
|
||||||
|
|
||||||
const handler = useCallback(
|
// export const useRoomAccountData = <T extends {}>(room: Room, eventType: string): T => {
|
||||||
(event) => {
|
// const [value, setValue] = useState<T | undefined>(() => tryGetContent<T>(room.getAccountData(eventType)));
|
||||||
if (event.getType() !== eventType) return;
|
|
||||||
setValue(event.getContent());
|
|
||||||
},
|
|
||||||
[eventType],
|
|
||||||
);
|
|
||||||
useTypedEventEmitter(room, RoomEvent.AccountData, handler);
|
|
||||||
|
|
||||||
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",
|
"Room %(name)s": "Room %(name)s",
|
||||||
"Recently visited rooms": "Recently visited rooms",
|
"Recently visited rooms": "Recently visited rooms",
|
||||||
"No recently visited rooms": "No 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",
|
"Video room": "Video room",
|
||||||
"Public space": "Public space",
|
"Public space": "Public space",
|
||||||
"Public room": "Public room",
|
"Public room": "Public room",
|
||||||
"Private space": "Private space",
|
"Private space": "Private space",
|
||||||
"Private room": "Private room",
|
"Private room": "Private room",
|
||||||
"%(count)s members": {
|
|
||||||
"other": "%(count)s members",
|
|
||||||
"one": "%(count)s member"
|
|
||||||
},
|
|
||||||
"Start new chat": "Start new chat",
|
"Start new chat": "Start new chat",
|
||||||
"Invite to space": "Invite to space",
|
"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",
|
"You do not have permissions to invite people to this space": "You do not have permissions to invite people to this space",
|
||||||
|
|
|
@ -23,7 +23,7 @@ describe("<FacePile />", () => {
|
||||||
const member = mkRoomMember("123", "456", "join");
|
const member = mkRoomMember("123", "456", "join");
|
||||||
|
|
||||||
const { asFragment } = render(
|
const { asFragment } = render(
|
||||||
<FacePile members={[member]} size="36px" overflow={false} tooltip={<>tooltip</>} />,
|
<FacePile members={[member]} size="36px" overflow={false} tooltipLabel="tooltip" />,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { render } from "@testing-library/react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { mkRoom, mkRoomMember, stubClient, withClientContextRenderOptions } from "../../../test-utils";
|
||||||
|
import RoomFacePile from "../../../../src/components/views/elements/RoomFacePile";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
|
||||||
|
describe("<RoomFacePile />", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const cli = stubClient();
|
||||||
|
DMRoomMap.makeShared(cli);
|
||||||
|
const room = mkRoom(cli, "!123");
|
||||||
|
|
||||||
|
jest.spyOn(room, "getJoinedMembers").mockReturnValue([mkRoomMember(room.roomId, "@bob:example.org", "join")]);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<RoomFacePile onlyKnownUsers={false} room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,11 +4,10 @@ exports[`<FacePile /> renders with a tooltip 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
class="mx_FacePile"
|
class="mx_FacePile"
|
||||||
|
data-state="closed"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-describedby="mx_TooltipTarget_vY7Q4uEh"
|
class="_stacked-avatars_2lhia_116"
|
||||||
class="mx_TextWithTooltip_target mx_FacePile_faces"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"
|
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`<RoomFacePile /> renders 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_FacePile"
|
||||||
|
data-state="closed"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="_stacked-avatars_2lhia_116"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"
|
||||||
|
data-color="8"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
data-type="round"
|
||||||
|
role="presentation"
|
||||||
|
style="--cpd-avatar-size: 28px;"
|
||||||
|
title=""
|
||||||
|
>
|
||||||
|
b
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
|
@ -15,12 +15,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { getAllByTitle, getByText, getByTitle, render, screen } from "@testing-library/react";
|
|
||||||
import { Room, EventType, MatrixEvent, PendingEventOrdering, MatrixCall } from "matrix-js-sdk/src/matrix";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { EventType, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { getAllByTitle, getByLabelText, getByText, getByTitle, render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
import { stubClient } from "../../../test-utils";
|
import { mkEvent, stubClient, withClientContextRenderOptions } from "../../../test-utils";
|
||||||
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
@ -33,7 +33,7 @@ import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
import { CallStore } from "../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../src/stores/CallStore";
|
||||||
import { Call, ElementCall } from "../../../../src/models/Call";
|
import { Call, ElementCall } from "../../../../src/models/Call";
|
||||||
|
|
||||||
describe("Roomeader", () => {
|
describe("RoomHeader", () => {
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
||||||
const ROOM_ID = "!1:example.org";
|
const ROOM_ID = "!1:example.org";
|
||||||
|
@ -57,7 +57,10 @@ describe("Roomeader", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders the room header", () => {
|
it("renders the room header", () => {
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
expect(container).toHaveTextContent(ROOM_ID);
|
expect(container).toHaveTextContent(ROOM_ID);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -75,26 +78,129 @@ describe("Roomeader", () => {
|
||||||
});
|
});
|
||||||
await room.addLiveEvents([roomTopic]);
|
await room.addLiveEvents([roomTopic]);
|
||||||
|
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
expect(container).toHaveTextContent(TOPIC);
|
expect(container).toHaveTextContent(TOPIC);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens the room summary", async () => {
|
it("opens the room summary", async () => {
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
await userEvent.click(getByText(container, ROOM_ID));
|
await userEvent.click(getByText(container, ROOM_ID));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not show the face pile for DMs", () => {
|
||||||
|
const client = MatrixClientPeg.get()!;
|
||||||
|
|
||||||
|
jest.spyOn(client, "getAccountData").mockReturnValue(
|
||||||
|
mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.Direct,
|
||||||
|
user: client.getSafeUserId(),
|
||||||
|
content: {
|
||||||
|
"user@example.com": [room.roomId],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
userId: "@me:example.org",
|
||||||
|
name: "Member",
|
||||||
|
rawDisplayName: "Member",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: "join",
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { asFragment } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a face pile for rooms", async () => {
|
||||||
|
const members = [
|
||||||
|
{
|
||||||
|
userId: "@me:example.org",
|
||||||
|
name: "Member",
|
||||||
|
rawDisplayName: "Member",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: "join",
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@you:example.org",
|
||||||
|
name: "Member",
|
||||||
|
rawDisplayName: "Member",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: "join",
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@them:example.org",
|
||||||
|
name: "Member",
|
||||||
|
rawDisplayName: "Member",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: "join",
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@bot:example.org",
|
||||||
|
name: "Bot user",
|
||||||
|
rawDisplayName: "Bot user",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: "join",
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
room.currentState.setJoinedMemberCount(members.length);
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue(members);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(container).toHaveTextContent("4");
|
||||||
|
|
||||||
|
const facePile = getByLabelText(container, "4 members");
|
||||||
|
expect(facePile).toHaveTextContent("4");
|
||||||
|
|
||||||
|
await userEvent.click(facePile);
|
||||||
|
|
||||||
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
|
||||||
|
});
|
||||||
|
|
||||||
it("opens the thread panel", async () => {
|
it("opens the thread panel", async () => {
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
await userEvent.click(getByTitle(container, "Threads"));
|
await userEvent.click(getByTitle(container, "Threads"));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens the notifications panel", async () => {
|
it("opens the notifications panel", async () => {
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
await userEvent.click(getByTitle(container, "Notifications"));
|
await userEvent.click(getByTitle(container, "Notifications"));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
||||||
|
@ -103,7 +209,10 @@ describe("Roomeader", () => {
|
||||||
describe("groups call disabled", () => {
|
describe("groups call disabled", () => {
|
||||||
it("you can't call if you're alone", () => {
|
it("you can't call if you're alone", () => {
|
||||||
mockRoomMembers(room, 1);
|
mockRoomMembers(room, 1);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
for (const button of getAllByTitle(container, "There's no one here to call")) {
|
for (const button of getAllByTitle(container, "There's no one here to call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
|
@ -111,7 +220,10 @@ describe("Roomeader", () => {
|
||||||
|
|
||||||
it("you can call when you're two in the room", async () => {
|
it("you can call when you're two in the room", async () => {
|
||||||
mockRoomMembers(room, 2);
|
mockRoomMembers(room, 2);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByTitle(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByTitle(container, "Video call");
|
||||||
expect(voiceButton).not.toBeDisabled();
|
expect(voiceButton).not.toBeDisabled();
|
||||||
|
@ -132,7 +244,10 @@ describe("Roomeader", () => {
|
||||||
// The JS-SDK does not export the class `MatrixCall` only the type
|
// The JS-SDK does not export the class `MatrixCall` only the type
|
||||||
{} as MatrixCall,
|
{} as MatrixCall,
|
||||||
);
|
);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
for (const button of getAllByTitle(container, "Ongoing call")) {
|
for (const button of getAllByTitle(container, "Ongoing call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
|
@ -141,7 +256,10 @@ describe("Roomeader", () => {
|
||||||
it("can calls in large rooms if able to edit widgets", () => {
|
it("can calls in large rooms if able to edit widgets", () => {
|
||||||
mockRoomMembers(room, 10);
|
mockRoomMembers(room, 10);
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
expect(getByTitle(container, "Voice call")).not.toBeDisabled();
|
expect(getByTitle(container, "Voice call")).not.toBeDisabled();
|
||||||
expect(getByTitle(container, "Video call")).not.toBeDisabled();
|
expect(getByTitle(container, "Video call")).not.toBeDisabled();
|
||||||
|
@ -150,7 +268,10 @@ describe("Roomeader", () => {
|
||||||
it("disable calls in large rooms by default", () => {
|
it("disable calls in large rooms by default", () => {
|
||||||
mockRoomMembers(room, 10);
|
mockRoomMembers(room, 10);
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(false);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
expect(getByTitle(container, "You do not have permission to start voice calls")).toBeDisabled();
|
expect(getByTitle(container, "You do not have permission to start voice calls")).toBeDisabled();
|
||||||
expect(getByTitle(container, "You do not have permission to start video calls")).toBeDisabled();
|
expect(getByTitle(container, "You do not have permission to start video calls")).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
@ -166,7 +287,10 @@ describe("Roomeader", () => {
|
||||||
// allow element calls
|
// allow element calls
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockReturnValue(true);
|
||||||
|
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
expect(screen.queryByTitle("Voice call")).toBeNull();
|
expect(screen.queryByTitle("Voice call")).toBeNull();
|
||||||
|
|
||||||
|
@ -187,7 +311,10 @@ describe("Roomeader", () => {
|
||||||
|
|
||||||
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({} as Call);
|
jest.spyOn(CallStore.instance, "getCall").mockReturnValue({} as Call);
|
||||||
|
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
expect(getByTitle(container, "Ongoing call")).toBeDisabled();
|
expect(getByTitle(container, "Ongoing call")).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -197,7 +324,10 @@ describe("Roomeader", () => {
|
||||||
// The JS-SDK does not export the class `MatrixCall` only the type
|
// The JS-SDK does not export the class `MatrixCall` only the type
|
||||||
{} as MatrixCall,
|
{} as MatrixCall,
|
||||||
);
|
);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
for (const button of getAllByTitle(container, "Ongoing call")) {
|
for (const button of getAllByTitle(container, "Ongoing call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
|
@ -205,7 +335,10 @@ describe("Roomeader", () => {
|
||||||
|
|
||||||
it("can't call if you have no friends", () => {
|
it("can't call if you have no friends", () => {
|
||||||
mockRoomMembers(room, 1);
|
mockRoomMembers(room, 1);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
for (const button of getAllByTitle(container, "There's no one here to call")) {
|
for (const button of getAllByTitle(container, "There's no one here to call")) {
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
}
|
}
|
||||||
|
@ -213,7 +346,10 @@ describe("Roomeader", () => {
|
||||||
|
|
||||||
it("calls using legacy or jitsi", async () => {
|
it("calls using legacy or jitsi", async () => {
|
||||||
mockRoomMembers(room, 2);
|
mockRoomMembers(room, 2);
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByTitle(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByTitle(container, "Video call");
|
||||||
|
@ -236,7 +372,10 @@ describe("Roomeader", () => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByTitle(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByTitle(container, "Video call");
|
||||||
|
@ -260,7 +399,10 @@ describe("Roomeader", () => {
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { container } = render(<RoomHeader room={room} />);
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
const voiceButton = getByTitle(container, "Voice call");
|
const voiceButton = getByTitle(container, "Voice call");
|
||||||
const videoButton = getByTitle(container, "Video call");
|
const videoButton = getByTitle(container, "Video call");
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<header
|
||||||
|
class="mx_Flex mx_RoomHeader light-panel"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="_avatar_2lhia_17 mx_BaseAvatar _avatar-imageless_2lhia_56"
|
||||||
|
data-color="7"
|
||||||
|
data-testid="avatar-img"
|
||||||
|
data-type="round"
|
||||||
|
role="presentation"
|
||||||
|
style="--cpd-avatar-size: 40px;"
|
||||||
|
title=""
|
||||||
|
>
|
||||||
|
!
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_Box mx_RoomHeader_info mx_Box--flex"
|
||||||
|
style="--mx-box-flex: 1;"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-level="1"
|
||||||
|
class="_font-body-lg-semibold_1g2sj_89"
|
||||||
|
dir="auto"
|
||||||
|
role="heading"
|
||||||
|
title="!1:example.org"
|
||||||
|
>
|
||||||
|
!1:example.org
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="mx_Flex"
|
||||||
|
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="_icon-button_yvmcf_17"
|
||||||
|
disabled=""
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
title="There's no one here to call"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_icon-button_yvmcf_17"
|
||||||
|
disabled=""
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
title="There's no one here to call"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_icon-button_yvmcf_17"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
title="Threads"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="_icon-button_yvmcf_17"
|
||||||
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
title="Notifications"
|
||||||
|
>
|
||||||
|
<div />
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
Loading…
Reference in New Issue