Add missing presence indicator to new room header (#12865)
* Add missing presence indicator to new room header DecoratedRoomAvatar doesn't match Figma styles so created a composable avatar wrapper Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add oobData to new room header avatar Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Iterate Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Simplify Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Improve coverage Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
ca8d63af37
commit
dde19f36ac
9 changed files with 414 additions and 67 deletions
|
@ -117,6 +117,7 @@
|
||||||
@import "./views/avatars/_BaseAvatar.pcss";
|
@import "./views/avatars/_BaseAvatar.pcss";
|
||||||
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
|
@import "./views/avatars/_DecoratedRoomAvatar.pcss";
|
||||||
@import "./views/avatars/_WidgetAvatar.pcss";
|
@import "./views/avatars/_WidgetAvatar.pcss";
|
||||||
|
@import "./views/avatars/_WithPresenceIndicator.pcss";
|
||||||
@import "./views/beta/_BetaCard.pcss";
|
@import "./views/beta/_BetaCard.pcss";
|
||||||
@import "./views/context_menus/_DeviceContextMenu.pcss";
|
@import "./views/context_menus/_DeviceContextMenu.pcss";
|
||||||
@import "./views/context_menus/_IconizedContextMenu.pcss";
|
@import "./views/context_menus/_IconizedContextMenu.pcss";
|
||||||
|
|
54
res/css/views/avatars/_WithPresenceIndicator.pcss
Normal file
54
res/css/views/avatars/_WithPresenceIndicator.pcss
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator {
|
||||||
|
position: relative;
|
||||||
|
contain: content;
|
||||||
|
line-height: 0;
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator_icon {
|
||||||
|
position: absolute;
|
||||||
|
right: -2px;
|
||||||
|
bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator_icon::before {
|
||||||
|
content: "";
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid var(--cpd-color-bg-canvas-default);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator_icon_offline::before {
|
||||||
|
background-color: $presence-offline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator_icon_online::before {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator_icon_away::before {
|
||||||
|
background-color: $presence-away;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_WithPresenceIndicator_icon_busy::before {
|
||||||
|
background-color: $presence-busy;
|
||||||
|
}
|
||||||
|
}
|
141
src/components/views/avatars/WithPresenceIndicator.tsx
Normal file
141
src/components/views/avatars/WithPresenceIndicator.tsx
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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 React, { ReactNode, useEffect, useState } from "react";
|
||||||
|
import { ClientEvent, Room, RoomMember, RoomStateEvent, UserEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Tooltip } from "@vector-im/compound-web";
|
||||||
|
|
||||||
|
import { isPresenceEnabled } from "../../../utils/presence";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
|
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||||
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
import { BUSY_PRESENCE_NAME } from "../rooms/PresenceLabel";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
room: Room;
|
||||||
|
size: string; // CSS size
|
||||||
|
tooltipProps?: {
|
||||||
|
tabIndex?: number;
|
||||||
|
};
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Presence {
|
||||||
|
// Note: the names here are used in CSS class names
|
||||||
|
Online = "ONLINE",
|
||||||
|
Away = "AWAY",
|
||||||
|
Offline = "OFFLINE",
|
||||||
|
Busy = "BUSY",
|
||||||
|
}
|
||||||
|
|
||||||
|
function tooltipText(variant: Presence): string {
|
||||||
|
switch (variant) {
|
||||||
|
case Presence.Online:
|
||||||
|
return _t("presence|online");
|
||||||
|
case Presence.Away:
|
||||||
|
return _t("presence|away");
|
||||||
|
case Presence.Offline:
|
||||||
|
return _t("presence|offline");
|
||||||
|
case Presence.Busy:
|
||||||
|
return _t("presence|busy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDmMember(room: Room): RoomMember | null {
|
||||||
|
const otherUserId = DMRoomMap.shared().getUserIdForRoomId(room.roomId);
|
||||||
|
return otherUserId ? room.getMember(otherUserId) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDmMember = (room: Room): RoomMember | null => {
|
||||||
|
const [dmMember, setDmMember] = useState<RoomMember | null>(getDmMember(room));
|
||||||
|
const updateDmMember = (): void => {
|
||||||
|
setDmMember(getDmMember(room));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEventEmitter(room.currentState, RoomStateEvent.Members, updateDmMember);
|
||||||
|
useEventEmitter(room.client, ClientEvent.AccountData, updateDmMember);
|
||||||
|
useEffect(updateDmMember, [room]);
|
||||||
|
|
||||||
|
return dmMember;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getPresence(member: RoomMember | null): Presence | null {
|
||||||
|
if (!member?.user) return null;
|
||||||
|
|
||||||
|
const presence = member.user.presence;
|
||||||
|
const isOnline = member.user.currentlyActive || presence === "online";
|
||||||
|
if (BUSY_PRESENCE_NAME.matches(member.user.presence)) {
|
||||||
|
return Presence.Busy;
|
||||||
|
}
|
||||||
|
if (isOnline) {
|
||||||
|
return Presence.Online;
|
||||||
|
}
|
||||||
|
if (presence === "offline") {
|
||||||
|
return Presence.Offline;
|
||||||
|
}
|
||||||
|
if (presence === "unavailable") {
|
||||||
|
return Presence.Away;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const usePresence = (room: Room, member: RoomMember | null): Presence | null => {
|
||||||
|
const [presence, setPresence] = useState<Presence | null>(getPresence(member));
|
||||||
|
const updatePresence = (): void => {
|
||||||
|
setPresence(getPresence(member));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEventEmitter(member?.user, UserEvent.Presence, updatePresence);
|
||||||
|
useEventEmitter(member?.user, UserEvent.CurrentlyActive, updatePresence);
|
||||||
|
useEffect(updatePresence, [member]);
|
||||||
|
|
||||||
|
if (getJoinedNonFunctionalMembers(room).length !== 2 || !isPresenceEnabled(room.client)) return null;
|
||||||
|
return presence;
|
||||||
|
};
|
||||||
|
|
||||||
|
const WithPresenceIndicator: React.FC<Props> = ({ room, size, tooltipProps, children }) => {
|
||||||
|
const dmMember = useDmMember(room);
|
||||||
|
const presence = usePresence(room, dmMember);
|
||||||
|
|
||||||
|
let icon: JSX.Element | undefined;
|
||||||
|
if (presence) {
|
||||||
|
icon = (
|
||||||
|
<div
|
||||||
|
tabIndex={tooltipProps?.tabIndex ?? 0}
|
||||||
|
className={`mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_${presence.toLowerCase()}`}
|
||||||
|
style={{
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!presence) return <>{children}</>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx_WithPresenceIndicator">
|
||||||
|
{children}
|
||||||
|
<Tooltip label={tooltipText(presence)} placement="bottom">
|
||||||
|
{icon}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WithPresenceIndicator;
|
|
@ -21,7 +21,7 @@ import classNames from "classnames";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { formatDuration } from "../../../DateUtils";
|
import { formatDuration } from "../../../DateUtils";
|
||||||
|
|
||||||
const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
export const BUSY_PRESENCE_NAME = new UnstableValue("busy", "org.matrix.msc3026.busy");
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// number of milliseconds ago this user was last active.
|
// number of milliseconds ago this user was last active.
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
import React, { useCallback, useMemo, useState } from "react";
|
||||||
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
import { Body as BodyText, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web";
|
||||||
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
|
||||||
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
|
||||||
|
@ -25,12 +25,11 @@ import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/ico
|
||||||
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
|
import VerifiedIcon from "@vector-im/compound-design-tokens/assets/web/icons/verified";
|
||||||
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
import ErrorIcon from "@vector-im/compound-design-tokens/assets/web/icons/error";
|
||||||
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
import PublicIcon from "@vector-im/compound-design-tokens/assets/web/icons/public";
|
||||||
import { EventType, JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
import { JoinRule, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
|
||||||
import { useRoomName } from "../../../hooks/useRoomName";
|
import { useRoomName } from "../../../hooks/useRoomName";
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
import { useAccountData } from "../../../hooks/useAccountData";
|
|
||||||
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
|
||||||
import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMembers";
|
import { useRoomMemberCount, useRoomMembers } from "../../../hooks/useRoomMembers";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -58,18 +57,22 @@ import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
|
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement";
|
||||||
import { useIsReleaseAnnouncementOpen } from "../../../hooks/useIsReleaseAnnouncementOpen";
|
import { useIsReleaseAnnouncementOpen } from "../../../hooks/useIsReleaseAnnouncementOpen";
|
||||||
import { ReleaseAnnouncementStore } from "../../../stores/ReleaseAnnouncementStore";
|
import { ReleaseAnnouncementStore } from "../../../stores/ReleaseAnnouncementStore";
|
||||||
|
import WithPresenceIndicator, { useDmMember } from "../avatars/WithPresenceIndicator";
|
||||||
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
|
||||||
export default function RoomHeader({
|
export default function RoomHeader({
|
||||||
room,
|
room,
|
||||||
additionalButtons,
|
additionalButtons,
|
||||||
|
oobData,
|
||||||
}: {
|
}: {
|
||||||
room: Room;
|
room: Room;
|
||||||
additionalButtons?: ViewRoomOpts["buttons"];
|
additionalButtons?: ViewRoomOpts["buttons"];
|
||||||
|
oobData?: IOOBData;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const client = useMatrixClientContext();
|
const client = useMatrixClientContext();
|
||||||
|
|
||||||
const roomName = useRoomName(room);
|
const roomName = useRoomName(room);
|
||||||
const roomState = useRoomState(room);
|
const joinRule = useRoomState(room, (state) => state.getJoinRule());
|
||||||
|
|
||||||
const members = useRoomMembers(room, 2500);
|
const members = useRoomMembers(room, 2500);
|
||||||
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
const memberCount = useRoomMemberCount(room, { throttleWait: 2500 });
|
||||||
|
@ -100,16 +103,8 @@ export default function RoomHeader({
|
||||||
const threadNotifications = useRoomThreadNotifications(room);
|
const threadNotifications = useRoomThreadNotifications(room);
|
||||||
const globalNotificationState = useGlobalNotificationState();
|
const globalNotificationState = useGlobalNotificationState();
|
||||||
|
|
||||||
const directRoomsList = useAccountData<Record<string, string[]>>(client, EventType.Direct);
|
const dmMember = useDmMember(room);
|
||||||
const [isDirectMessage, setDirectMessage] = useState(false);
|
const isDirectMessage = !!dmMember;
|
||||||
useEffect(() => {
|
|
||||||
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
|
|
||||||
if (dmRoomList.includes(room?.roomId ?? "")) {
|
|
||||||
setDirectMessage(true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [room, directRoomsList]);
|
|
||||||
const e2eStatus = useEncryptionStatus(client, room);
|
const e2eStatus = useEncryptionStatus(client, room);
|
||||||
|
|
||||||
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
const notificationsEnabled = useFeatureEnabled("feature_notifications");
|
||||||
|
@ -259,7 +254,9 @@ export default function RoomHeader({
|
||||||
}}
|
}}
|
||||||
className="mx_RoomHeader_infoWrapper"
|
className="mx_RoomHeader_infoWrapper"
|
||||||
>
|
>
|
||||||
<RoomAvatar room={room} size="40px" />
|
<WithPresenceIndicator room={room} size="8px">
|
||||||
|
<RoomAvatar room={room} size="40px" oobData={oobData} />
|
||||||
|
</WithPresenceIndicator>
|
||||||
<Box flex="1" className="mx_RoomHeader_info">
|
<Box flex="1" className="mx_RoomHeader_info">
|
||||||
<BodyText
|
<BodyText
|
||||||
as="div"
|
as="div"
|
||||||
|
@ -272,7 +269,7 @@ export default function RoomHeader({
|
||||||
>
|
>
|
||||||
<span className="mx_RoomHeader_truncated mx_lineClamp">{roomName}</span>
|
<span className="mx_RoomHeader_truncated mx_lineClamp">{roomName}</span>
|
||||||
|
|
||||||
{!isDirectMessage && roomState.getJoinRule() === JoinRule.Public && (
|
{!isDirectMessage && joinRule === JoinRule.Public && (
|
||||||
<Tooltip label={_t("common|public_room")} placement="right">
|
<Tooltip label={_t("common|public_room")} placement="right">
|
||||||
<PublicIcon
|
<PublicIcon
|
||||||
width="16px"
|
width="16px"
|
||||||
|
|
110
test/components/views/avatars/WithPresenceIndicator-test.tsx
Normal file
110
test/components/views/avatars/WithPresenceIndicator-test.tsx
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024 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, waitFor } from "@testing-library/react";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
import { MatrixClient, PendingEventOrdering, Room, RoomMember, User } from "matrix-js-sdk/src/matrix";
|
||||||
|
import React from "react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import { stubClient } from "../../../test-utils";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import WithPresenceIndicator from "../../../../src/components/views/avatars/WithPresenceIndicator";
|
||||||
|
import { isPresenceEnabled } from "../../../../src/utils/presence";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/utils/presence");
|
||||||
|
|
||||||
|
jest.mock("../../../../src/utils/room/getJoinedNonFunctionalMembers", () => ({
|
||||||
|
getJoinedNonFunctionalMembers: jest.fn().mockReturnValue([1, 2]),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("WithPresenceIndicator", () => {
|
||||||
|
const ROOM_ID = "roomId";
|
||||||
|
|
||||||
|
let mockClient: MatrixClient;
|
||||||
|
let room: Room;
|
||||||
|
|
||||||
|
function renderComponent() {
|
||||||
|
return render(
|
||||||
|
<WithPresenceIndicator room={room} size="32px">
|
||||||
|
<span />
|
||||||
|
</WithPresenceIndicator>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
stubClient();
|
||||||
|
mockClient = mocked(MatrixClientPeg.safeGet());
|
||||||
|
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dmRoomMap = {
|
||||||
|
getUserIdForRoomId: jest.fn(),
|
||||||
|
} as unknown as DMRoomMap;
|
||||||
|
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders only child if presence is disabled", async () => {
|
||||||
|
mocked(isPresenceEnabled).mockReturnValue(false);
|
||||||
|
const { container } = renderComponent();
|
||||||
|
|
||||||
|
expect(container.children).toHaveLength(1);
|
||||||
|
expect(container.children[0].tagName).toBe("SPAN");
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["online", "Online"],
|
||||||
|
["offline", "Offline"],
|
||||||
|
["unavailable", "Away"],
|
||||||
|
])("renders presence indicator with tooltip for DM rooms", async (presenceStr, renderedStr) => {
|
||||||
|
mocked(isPresenceEnabled).mockReturnValue(true);
|
||||||
|
const DM_USER_ID = "@bob:foo.bar";
|
||||||
|
const dmRoomMap = {
|
||||||
|
getUserIdForRoomId: () => {
|
||||||
|
return DM_USER_ID;
|
||||||
|
},
|
||||||
|
} as unknown as DMRoomMap;
|
||||||
|
jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
|
||||||
|
room.getMember = jest.fn((userId) => {
|
||||||
|
const member = new RoomMember(room.roomId, userId);
|
||||||
|
member.user = new User(userId);
|
||||||
|
member.user.presence = presenceStr;
|
||||||
|
return member;
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container, asFragment } = renderComponent();
|
||||||
|
|
||||||
|
const presence = container.querySelector(".mx_WithPresenceIndicator_icon")!;
|
||||||
|
expect(presence).toBeVisible();
|
||||||
|
await userEvent.hover(presence!);
|
||||||
|
|
||||||
|
// wait for the tooltip to open
|
||||||
|
const tooltip = await waitFor(() => {
|
||||||
|
const tooltip = document.getElementById(presence.getAttribute("aria-describedby")!);
|
||||||
|
expect(tooltip).toBeVisible();
|
||||||
|
return tooltip;
|
||||||
|
});
|
||||||
|
expect(tooltip).toHaveTextContent(renderedStr);
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_WithPresenceIndicator"
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
<div
|
||||||
|
aria-describedby="floating-ui-2"
|
||||||
|
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_online"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 2`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_WithPresenceIndicator"
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
<div
|
||||||
|
aria-describedby="floating-ui-8"
|
||||||
|
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_offline"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`WithPresenceIndicator renders presence indicator with tooltip for DM rooms 3`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<div
|
||||||
|
class="mx_WithPresenceIndicator"
|
||||||
|
>
|
||||||
|
<span />
|
||||||
|
<div
|
||||||
|
aria-describedby="floating-ui-14"
|
||||||
|
class="mx_WithPresenceIndicator_icon mx_WithPresenceIndicator_icon_away"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
|
@ -40,8 +40,9 @@ import {
|
||||||
waitFor,
|
waitFor,
|
||||||
} from "@testing-library/react";
|
} from "@testing-library/react";
|
||||||
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
import { ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
|
||||||
import { filterConsole, mkEvent, stubClient } from "../../../test-utils";
|
import { filterConsole, stubClient } 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";
|
||||||
|
@ -111,37 +112,6 @@ describe("RoomHeader", () => {
|
||||||
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: KnownMembership.Join,
|
|
||||||
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
||||||
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const { asFragment } = render(<RoomHeader room={room} />, getWrapper());
|
|
||||||
|
|
||||||
expect(asFragment()).toMatchSnapshot();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows a face pile for rooms", async () => {
|
it("shows a face pile for rooms", async () => {
|
||||||
const members = [
|
const members = [
|
||||||
{
|
{
|
||||||
|
@ -620,20 +590,30 @@ describe("RoomHeader", () => {
|
||||||
client = MatrixClientPeg.get()!;
|
client = MatrixClientPeg.get()!;
|
||||||
|
|
||||||
// Make the mocked room a DM
|
// Make the mocked room a DM
|
||||||
jest.spyOn(client, "getAccountData").mockImplementation((eventType: string): MatrixEvent | undefined => {
|
mocked(DMRoomMap.shared().getUserIdForRoomId).mockImplementation((roomId) => {
|
||||||
if (eventType === EventType.Direct) {
|
if (roomId === room.roomId) return "@user:example.com";
|
||||||
return mkEvent({
|
|
||||||
event: true,
|
|
||||||
content: {
|
|
||||||
[client.getUserId()!]: [room.roomId],
|
|
||||||
},
|
|
||||||
type: EventType.Direct,
|
|
||||||
user: client.getSafeUserId(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
});
|
||||||
|
room.getMember = jest.fn((userId) => new RoomMember(room.roomId, userId));
|
||||||
|
room.getJoinedMembers = jest.fn().mockReturnValue([
|
||||||
|
{
|
||||||
|
userId: "@me:example.org",
|
||||||
|
name: "Member",
|
||||||
|
rawDisplayName: "Member",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: KnownMembership.Join,
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
userId: "@bob:example.org",
|
||||||
|
name: "Other Member",
|
||||||
|
rawDisplayName: "Other Member",
|
||||||
|
roomId: room.roomId,
|
||||||
|
membership: KnownMembership.Join,
|
||||||
|
getAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
|
||||||
|
},
|
||||||
|
]);
|
||||||
jest.spyOn(client, "isCryptoEnabled").mockReturnValue(true);
|
jest.spyOn(client, "isCryptoEnabled").mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -647,6 +627,12 @@ describe("RoomHeader", () => {
|
||||||
|
|
||||||
await waitFor(() => expect(getByLabelText(container, expectedLabel)).toBeInTheDocument());
|
await waitFor(() => expect(getByLabelText(container, expectedLabel)).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not show the face pile for DMs", () => {
|
||||||
|
const { asFragment } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
|
expect(asFragment()).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders additionalButtons", async () => {
|
it("renders additionalButtons", async () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<header
|
<header
|
||||||
class="mx_Flex mx_RoomHeader light-panel"
|
class="mx_Flex mx_RoomHeader light-panel"
|
||||||
|
@ -46,8 +46,7 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
||||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
|
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
|
<button
|
||||||
aria-disabled="true"
|
aria-label="Close lobby"
|
||||||
aria-label="There's no one here to call"
|
|
||||||
class="_icon-button_bh2qc_17"
|
class="_icon-button_bh2qc_17"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
@ -55,9 +54,19 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_133tf_26"
|
class="_indicator-icon_133tf_26"
|
||||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
style="--cpd-icon-button-size: 100%;"
|
||||||
>
|
>
|
||||||
<div />
|
<svg
|
||||||
|
fill="currentColor"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="1em"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
|
|
Loading…
Reference in a new issue