mirror of
https://github.com/element-hq/element-web
synced 2024-11-28 20:38:55 +03:00
Add E2E status in room header (#11493)
* Add E2E status in room header * Clearer logic for dmRoomList Co-authored-by: Andy Balaam <andy.balaam@matrix.org> * Add test for E2E shield * Remove dead code --------- Co-authored-by: Andy Balaam <andy.balaam@matrix.org>
This commit is contained in:
parent
6b3243b27b
commit
46037d2357
6 changed files with 127 additions and 17 deletions
|
@ -134,6 +134,14 @@ code {
|
||||||
color: $muted-fg-color;
|
color: $muted-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_Verified {
|
||||||
|
color: $e2e-verified-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Untrusted {
|
||||||
|
color: $e2e-warning-color;
|
||||||
|
}
|
||||||
|
|
||||||
b {
|
b {
|
||||||
/* On Firefox, the default weight for `<b>` is `bolder` which results in no bold */
|
/* On Firefox, the default weight for `<b>` is `bolder` which results in no bold */
|
||||||
/* effect since we only have specific weights of our fonts available. */
|
/* effect since we only have specific weights of our fonts available. */
|
||||||
|
|
|
@ -14,16 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useCallback, useMemo } from "react";
|
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Body as BodyText, IconButton } from "@vector-im/compound-web";
|
import { Body as BodyText, IconButton, Tooltip } from "@vector-im/compound-web";
|
||||||
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.svg";
|
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.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";
|
||||||
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 { Icon as VerifiedIcon } from "@vector-im/compound-design-tokens/icons/verified.svg";
|
||||||
|
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.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 { EventType, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
|
||||||
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";
|
||||||
|
@ -45,6 +46,8 @@ 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 { useEncryptionStatus } from "../../../hooks/useEncryptionStatus";
|
||||||
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
import FacePile from "../elements/FacePile";
|
import FacePile from "../elements/FacePile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -80,16 +83,6 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
const members = useRoomMembers(room);
|
const members = useRoomMembers(room);
|
||||||
const memberCount = useRoomMemberCount(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");
|
||||||
|
@ -132,6 +125,18 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
const threadNotifications = useRoomThreadNotifications(room);
|
const threadNotifications = useRoomThreadNotifications(room);
|
||||||
const globalNotificationState = useGlobalNotificationState();
|
const globalNotificationState = useGlobalNotificationState();
|
||||||
|
|
||||||
|
const directRoomsList = useAccountData<Record<string, string[]>>(client, EventType.Direct);
|
||||||
|
const [isDirectMessage, setDirectMessage] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
for (const [, dmRoomList] of Object.entries(directRoomsList)) {
|
||||||
|
if (dmRoomList.includes(room?.roomId ?? "")) {
|
||||||
|
setDirectMessage(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [room, directRoomsList]);
|
||||||
|
const e2eStatus = useEncryptionStatus(client, room);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
as="header"
|
as="header"
|
||||||
|
@ -154,6 +159,28 @@ export default function RoomHeader({ room }: { room: Room }): JSX.Element {
|
||||||
aria-level={1}
|
aria-level={1}
|
||||||
>
|
>
|
||||||
{roomName}
|
{roomName}
|
||||||
|
|
||||||
|
{isDirectMessage && e2eStatus === E2EStatus.Verified && (
|
||||||
|
<Tooltip label={_t("Verified")}>
|
||||||
|
<VerifiedIcon
|
||||||
|
width="16px"
|
||||||
|
height="16px"
|
||||||
|
className="mx_Verified"
|
||||||
|
aria-label={_t("Verified")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isDirectMessage && e2eStatus === E2EStatus.Warning && (
|
||||||
|
<Tooltip label={_t("Untrusted")}>
|
||||||
|
<ErrorIcon
|
||||||
|
width="16px"
|
||||||
|
height="16px"
|
||||||
|
className="mx_Untrusted"
|
||||||
|
aria-label={_t("Untrusted")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
</BodyText>
|
</BodyText>
|
||||||
{roomTopic && (
|
{roomTopic && (
|
||||||
<BodyText as="div" size="sm" className="mx_RoomHeader_topic">
|
<BodyText as="div" size="sm" className="mx_RoomHeader_topic">
|
||||||
|
|
|
@ -37,7 +37,6 @@ export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: strin
|
||||||
return value || ({} as T);
|
return value || ({} as T);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Hook to simplify listening to Matrix room account data
|
|
||||||
// Currently not used, commenting out otherwise the dead code CI is unhappy.
|
// Currently not used, commenting out otherwise the dead code CI is unhappy.
|
||||||
// But this code is valid and probably will be needed.
|
// But this code is valid and probably will be needed.
|
||||||
|
|
||||||
|
|
34
src/hooks/useEncryptionStatus.ts
Normal file
34
src/hooks/useEncryptionStatus.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { E2EStatus, shieldStatusForRoom } from "../utils/ShieldUtils";
|
||||||
|
|
||||||
|
export function useEncryptionStatus(client: MatrixClient, room: Room): E2EStatus | null {
|
||||||
|
const [e2eStatus, setE2eStatus] = useState<E2EStatus | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (client.isCryptoEnabled()) {
|
||||||
|
shieldStatusForRoom(client, room).then((e2eStatus) => {
|
||||||
|
setE2eStatus(e2eStatus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [client, room]);
|
||||||
|
|
||||||
|
return e2eStatus;
|
||||||
|
}
|
|
@ -1851,6 +1851,7 @@
|
||||||
"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",
|
||||||
|
"Untrusted": "Untrusted",
|
||||||
"%(count)s members": {
|
"%(count)s members": {
|
||||||
"other": "%(count)s members",
|
"other": "%(count)s members",
|
||||||
"one": "%(count)s member"
|
"one": "%(count)s member"
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { CallType, MatrixCall } 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 { EventType, MatrixClient, MatrixEvent, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { getAllByTitle, getByLabelText, getByText, getByTitle, render, screen } from "@testing-library/react";
|
import { getAllByTitle, getByLabelText, getByText, getByTitle, render, screen, waitFor } from "@testing-library/react";
|
||||||
|
|
||||||
import { mkEvent, stubClient, withClientContextRenderOptions } 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";
|
||||||
|
@ -32,6 +32,9 @@ import SdkConfig from "../../../../src/SdkConfig";
|
||||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
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";
|
||||||
|
import * as ShieldUtils from "../../../../src/utils/ShieldUtils";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/utils/ShieldUtils");
|
||||||
|
|
||||||
describe("RoomHeader", () => {
|
describe("RoomHeader", () => {
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
@ -418,6 +421,44 @@ describe("RoomHeader", () => {
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("dm", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
beforeEach(() => {
|
||||||
|
client = MatrixClientPeg.get()!;
|
||||||
|
|
||||||
|
// Make the mocked room a DM
|
||||||
|
jest.spyOn(client, "getAccountData").mockImplementation((eventType: string): MatrixEvent | undefined => {
|
||||||
|
if (eventType === EventType.Direct) {
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
content: {
|
||||||
|
[client.getUserId()!]: [room.roomId],
|
||||||
|
},
|
||||||
|
type: EventType.Direct,
|
||||||
|
user: client.getSafeUserId(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
jest.spyOn(client, "isCryptoEnabled").mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[ShieldUtils.E2EStatus.Verified, "Verified"],
|
||||||
|
[ShieldUtils.E2EStatus.Warning, "Untrusted"],
|
||||||
|
])("shows the %s icon", async (value: ShieldUtils.E2EStatus, expectedLabel: string) => {
|
||||||
|
jest.spyOn(ShieldUtils, "shieldStatusForRoom").mockResolvedValue(value);
|
||||||
|
|
||||||
|
const { container } = render(
|
||||||
|
<RoomHeader room={room} />,
|
||||||
|
withClientContextRenderOptions(MatrixClientPeg.get()!),
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(getByLabelText(container, expectedLabel)).toBeInTheDocument());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Reference in a new issue