Add feature flag 'feature_new_room_decoration_ui' and segrate legacy UI component (#11345)
* Move RoomHeader to LegacyRoomHeader * Create new RoomHeader component
This commit is contained in:
parent
89a92c6351
commit
6ae7c033d5
30 changed files with 2309 additions and 2103 deletions
|
@ -59,7 +59,7 @@ describe("Create Room", () => {
|
||||||
|
|
||||||
cy.url().should("contain", "/#/room/#test-room-1:localhost");
|
cy.url().should("contain", "/#/room/#test-room-1:localhost");
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
cy.findByText(name);
|
cy.findByText(name);
|
||||||
cy.findByText(topic);
|
cy.findByText(topic);
|
||||||
});
|
});
|
||||||
|
|
|
@ -166,8 +166,8 @@ describe("Invite dialog", function () {
|
||||||
|
|
||||||
// Assert that the hovered user name on invitation UI does not have background color
|
// Assert that the hovered user name on invitation UI does not have background color
|
||||||
// TODO: implement the test on room-header.spec.ts
|
// TODO: implement the test on room-header.spec.ts
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
cy.get(".mx_RoomHeader_name--textonly")
|
cy.get(".mx_LegacyRoomHeader_name--textonly")
|
||||||
.realHover()
|
.realHover()
|
||||||
.should("have.css", "background-color", "rgba(0, 0, 0, 0)");
|
.should("have.css", "background-color", "rgba(0, 0, 0, 0)");
|
||||||
});
|
});
|
||||||
|
|
|
@ -116,7 +116,7 @@ describe("Lazy Loading", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function openMemberlist(): void {
|
function openMemberlist(): void {
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
cy.findByRole("button", { name: "Room info" }).click();
|
cy.findByRole("button", { name: "Room info" }).click();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,7 @@ describe("1:1 chat room", () => {
|
||||||
|
|
||||||
it("should open new 1:1 chat room after leaving the old one", () => {
|
it("should open new 1:1 chat room after leaving the old one", () => {
|
||||||
// leave 1:1 chat room
|
// leave 1:1 chat room
|
||||||
cy.get(".mx_RoomHeader_nametext").within(() => {
|
cy.get(".mx_LegacyRoomHeader_nametext").within(() => {
|
||||||
cy.findByText(username).click();
|
cy.findByText(username).click();
|
||||||
});
|
});
|
||||||
cy.findByRole("menuitem", { name: "Leave" }).click();
|
cy.findByRole("menuitem", { name: "Leave" }).click();
|
||||||
|
@ -60,7 +60,7 @@ describe("1:1 chat room", () => {
|
||||||
|
|
||||||
// open new 1:1 chat room
|
// open new 1:1 chat room
|
||||||
cy.visit(`/#/user/${user2.userId}?action=chat`);
|
cy.visit(`/#/user/${user2.userId}?action=chat`);
|
||||||
cy.get(".mx_RoomHeader_nametext").within(() => {
|
cy.get(".mx_LegacyRoomHeader_nametext").within(() => {
|
||||||
cy.findByText(username);
|
cy.findByText(username);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -38,8 +38,8 @@ describe("Room Header", () => {
|
||||||
it("should render default buttons properly", () => {
|
it("should render default buttons properly", () => {
|
||||||
cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
|
cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
// Names (aria-label) of every button rendered on mx_RoomHeader by default
|
// Names (aria-label) of every button rendered on mx_LegacyRoomHeader by default
|
||||||
const expectedButtonNames = [
|
const expectedButtonNames = [
|
||||||
"Room options", // The room name button next to the room avatar, which renders dropdown menu on click
|
"Room options", // The room name button next to the room avatar, which renders dropdown menu on click
|
||||||
"Voice call",
|
"Voice call",
|
||||||
|
@ -55,11 +55,11 @@ describe("Room Header", () => {
|
||||||
cy.findByRole("button", { name }).should("be.visible");
|
cy.findByRole("button", { name }).should("be.visible");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assert that just those seven buttons exist on mx_RoomHeader by default
|
// Assert that just those seven buttons exist on mx_LegacyRoomHeader by default
|
||||||
cy.findAllByRole("button").should("have.length", 7);
|
cy.findAllByRole("button").should("have.length", 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").percySnapshotElement("Room header");
|
cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the pin button for pinned messages card", () => {
|
it("should render the pin button for pinned messages card", () => {
|
||||||
|
@ -73,7 +73,7 @@ describe("Room Header", () => {
|
||||||
|
|
||||||
cy.findByRole("menuitem", { name: "Pin" }).should("be.visible").click();
|
cy.findByRole("menuitem", { name: "Pin" }).should("be.visible").click();
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
cy.findByRole("button", { name: "Pinned messages" }).should("be.visible");
|
cy.findByRole("button", { name: "Pinned messages" }).should("be.visible");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -88,22 +88,22 @@ describe("Room Header", () => {
|
||||||
|
|
||||||
cy.createRoom({ name: LONG_ROOM_NAME }).viewRoomByName(LONG_ROOM_NAME);
|
cy.createRoom({ name: LONG_ROOM_NAME }).viewRoomByName(LONG_ROOM_NAME);
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
// Wait until the room name is set
|
// Wait until the room name is set
|
||||||
cy.get(".mx_RoomHeader_nametext").within(() => {
|
cy.get(".mx_LegacyRoomHeader_nametext").within(() => {
|
||||||
cy.findByText(LONG_ROOM_NAME).should("exist");
|
cy.findByText(LONG_ROOM_NAME).should("exist");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
|
// Assert the size of buttons on RoomHeader are specified and the buttons are not compressed
|
||||||
// Note these assertions do not check the size of mx_RoomHeader_name button
|
// Note these assertions do not check the size of mx_LegacyRoomHeader_name button
|
||||||
cy.get(".mx_RoomHeader_button")
|
cy.get(".mx_LegacyRoomHeader_button")
|
||||||
.should("have.length", 6)
|
.should("have.length", 6)
|
||||||
.should("be.visible")
|
.should("be.visible")
|
||||||
.should("have.css", "height", "32px")
|
.should("have.css", "height", "32px")
|
||||||
.should("have.css", "width", "32px");
|
.should("have.css", "width", "32px");
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with a long room name", {
|
cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with a long room name", {
|
||||||
widths: [300, 600], // Magic numbers to emulate the narrow RoomHeader on the actual UI
|
widths: [300, 600], // Magic numbers to emulate the narrow RoomHeader on the actual UI
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -111,7 +111,7 @@ describe("Room Header", () => {
|
||||||
it("should have buttons highlighted by being clicked", () => {
|
it("should have buttons highlighted by being clicked", () => {
|
||||||
cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
|
cy.createRoom({ name: "Test Room" }).viewRoomByName("Test Room");
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
// Check these buttons
|
// Check these buttons
|
||||||
const buttonsHighlighted = ["Threads", "Notifications", "Room info"];
|
const buttonsHighlighted = ["Threads", "Notifications", "Room info"];
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ describe("Room Header", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with a highlighted button");
|
cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with a highlighted button");
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with a video room", () => {
|
describe("with a video room", () => {
|
||||||
|
@ -144,7 +144,7 @@ describe("Room Header", () => {
|
||||||
it("should render buttons for room options, beta pill, invite, chat, and room info", () => {
|
it("should render buttons for room options, beta pill, invite, chat, and room info", () => {
|
||||||
createVideoRoom();
|
createVideoRoom();
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
// Names (aria-label) of the buttons on the video room header
|
// Names (aria-label) of the buttons on the video room header
|
||||||
const expectedButtonNames = [
|
const expectedButtonNames = [
|
||||||
"Room options",
|
"Room options",
|
||||||
|
@ -163,13 +163,13 @@ describe("Room Header", () => {
|
||||||
cy.findAllByRole("button").should("have.length", 7);
|
cy.findAllByRole("button").should("have.length", 7);
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with a video room");
|
cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with a video room");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render a working chat button which opens the timeline on a right panel", () => {
|
it("should render a working chat button which opens the timeline on a right panel", () => {
|
||||||
createVideoRoom();
|
createVideoRoom();
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").findByRole("button", { name: "Chat" }).click();
|
cy.get(".mx_LegacyRoomHeader").findByRole("button", { name: "Chat" }).click();
|
||||||
|
|
||||||
// Assert that the video is rendered
|
// Assert that the video is rendered
|
||||||
cy.get(".mx_CallView video").should("exist");
|
cy.get(".mx_CallView video").should("exist");
|
||||||
|
@ -250,20 +250,20 @@ describe("Room Header", () => {
|
||||||
// Assert that AppsDrawer is rendered
|
// Assert that AppsDrawer is rendered
|
||||||
cy.get(".mx_AppsDrawer").should("exist");
|
cy.get(".mx_AppsDrawer").should("exist");
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
// Assert that "Hide Widgets" button is rendered and aria-checked is set to true
|
// Assert that "Hide Widgets" button is rendered and aria-checked is set to true
|
||||||
cy.findByRole("button", { name: "Hide Widgets" })
|
cy.findByRole("button", { name: "Hide Widgets" })
|
||||||
.should("exist")
|
.should("exist")
|
||||||
.should("have.attr", "aria-checked", "true");
|
.should("have.attr", "aria-checked", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with apps button (highlighted)");
|
cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with apps button (highlighted)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support hiding a widget", () => {
|
it("should support hiding a widget", () => {
|
||||||
cy.get(".mx_AppsDrawer").should("exist");
|
cy.get(".mx_AppsDrawer").should("exist");
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
// Click the apps button to hide AppsDrawer
|
// Click the apps button to hide AppsDrawer
|
||||||
cy.findByRole("button", { name: "Hide Widgets" }).should("exist").click();
|
cy.findByRole("button", { name: "Hide Widgets" }).should("exist").click();
|
||||||
|
|
||||||
|
@ -276,7 +276,7 @@ describe("Room Header", () => {
|
||||||
// Assert that AppsDrawer is not rendered
|
// Assert that AppsDrawer is not rendered
|
||||||
cy.get(".mx_AppsDrawer").should("not.exist");
|
cy.get(".mx_AppsDrawer").should("not.exist");
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").percySnapshotElement("Room header - with apps button (not highlighted)");
|
cy.get(".mx_LegacyRoomHeader").percySnapshotElement("Room header - with apps button (not highlighted)");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -114,7 +114,7 @@ Cypress.Commands.add(
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
"roomHeaderName",
|
"roomHeaderName",
|
||||||
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
(options?: Partial<Loggable & Timeoutable & Withinable & Shadow>): Chainable<JQuery<HTMLElement>> => {
|
||||||
return cy.get(".mx_RoomHeader_nametext", options);
|
return cy.get(".mx_LegacyRoomHeader_nametext", options);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -204,7 +204,7 @@ describe("Spotlight", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// wait for the room to have the right name
|
// wait for the room to have the right name
|
||||||
cy.get(".mx_RoomHeader").within(() => {
|
cy.get(".mx_LegacyRoomHeader").within(() => {
|
||||||
cy.findByText(room1Name);
|
cy.findByText(room1Name);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -279,7 +279,7 @@ describe("Threads", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.findByRole("button", { name: "Threads" })
|
cy.findByRole("button", { name: "Threads" })
|
||||||
.should("have.class", "mx_RoomHeader_button--unread") // User asserts thread list unread indicator
|
.should("have.class", "mx_LegacyRoomHeader_button--unread") // User asserts thread list unread indicator
|
||||||
.click(); // User opens thread list
|
.click(); // User opens thread list
|
||||||
|
|
||||||
// User asserts thread with correct root & latest events & unread dot
|
// User asserts thread with correct root & latest events & unread dot
|
||||||
|
|
|
@ -755,7 +755,7 @@ describe("Timeline", () => {
|
||||||
sendEvent(roomId, true);
|
sendEvent(roomId, true);
|
||||||
cy.visit("/#/room/" + roomId);
|
cy.visit("/#/room/" + roomId);
|
||||||
|
|
||||||
cy.get(".mx_RoomHeader").findByRole("button", { name: "Search" }).click();
|
cy.get(".mx_LegacyRoomHeader").findByRole("button", { name: "Search" }).click();
|
||||||
|
|
||||||
cy.get(".mx_SearchBar").percySnapshotElement("Search bar on the timeline", {
|
cy.get(".mx_SearchBar").percySnapshotElement("Search bar on the timeline", {
|
||||||
// Emulate narrow timeline
|
// Emulate narrow timeline
|
||||||
|
@ -791,7 +791,7 @@ describe("Timeline", () => {
|
||||||
.should("have.class", "mx_TextualEvent");
|
.should("have.class", "mx_TextualEvent");
|
||||||
|
|
||||||
// Display the room search bar
|
// Display the room search bar
|
||||||
cy.get(".mx_RoomHeader").findByRole("button", { name: "Search" }).click();
|
cy.get(".mx_LegacyRoomHeader").findByRole("button", { name: "Search" }).click();
|
||||||
|
|
||||||
// Search the string to display both the message and TextualEvent on search results panel
|
// Search the string to display both the message and TextualEvent on search results panel
|
||||||
cy.get(".mx_SearchBar").within(() => {
|
cy.get(".mx_SearchBar").within(() => {
|
||||||
|
|
|
@ -275,6 +275,7 @@
|
||||||
@import "./views/rooms/_HistoryTile.pcss";
|
@import "./views/rooms/_HistoryTile.pcss";
|
||||||
@import "./views/rooms/_IRCLayout.pcss";
|
@import "./views/rooms/_IRCLayout.pcss";
|
||||||
@import "./views/rooms/_JumpToBottomButton.pcss";
|
@import "./views/rooms/_JumpToBottomButton.pcss";
|
||||||
|
@import "./views/rooms/_LegacyRoomHeader.pcss";
|
||||||
@import "./views/rooms/_LinkPreviewGroup.pcss";
|
@import "./views/rooms/_LinkPreviewGroup.pcss";
|
||||||
@import "./views/rooms/_LinkPreviewWidget.pcss";
|
@import "./views/rooms/_LinkPreviewWidget.pcss";
|
||||||
@import "./views/rooms/_LiveContentSummary.pcss";
|
@import "./views/rooms/_LiveContentSummary.pcss";
|
||||||
|
|
|
@ -27,7 +27,7 @@ limitations under the License.
|
||||||
/* The resizer should be centered: only half of the gap-width is handled by the right panel. */
|
/* The resizer should be centered: only half of the gap-width is handled by the right panel. */
|
||||||
/* The other half by the RoomView. */
|
/* The other half by the RoomView. */
|
||||||
padding-left: calc(var(--container-gap-width) / 2);
|
padding-left: calc(var(--container-gap-width) / 2);
|
||||||
height: calc(100vh - 51px); /* height of .mx_RoomHeader.light-panel */
|
height: calc(100vh - 51px); /* height of .mx_LegacyRoomHeader.light-panel */
|
||||||
|
|
||||||
&:hover .mx_ResizeHandle--horizontal::before {
|
&:hover .mx_ResizeHandle--horizontal::before {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -190,7 +190,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rooms with immersive content */
|
/* Rooms with immersive content */
|
||||||
.mx_RoomView_immersive .mx_RoomHeader_wrapper {
|
.mx_RoomView_immersive .mx_LegacyRoomHeader_wrapper {
|
||||||
border: unset;
|
border: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
333
res/css/views/rooms/_LegacyRoomHeader.pcss
Normal file
333
res/css/views/rooms/_LegacyRoomHeader.pcss
Normal file
|
@ -0,0 +1,333 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--RoomHeader-indicator-dot-size: 8px;
|
||||||
|
--RoomHeader-indicator-dot-offset: -3px;
|
||||||
|
--RoomHeader-indicator-pulseColor: $alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader {
|
||||||
|
flex: 0 0 50px;
|
||||||
|
border-bottom: 1px solid $primary-hairline-color;
|
||||||
|
background-color: $background;
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_icon {
|
||||||
|
height: 12px;
|
||||||
|
width: 12px;
|
||||||
|
|
||||||
|
&.mx_LegacyRoomHeader_icon_video {
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
background-color: $secondary-content;
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
||||||
|
mask-size: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_E2EIcon {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%; /* To give the tooltip room to breathe */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_CallDuration {
|
||||||
|
margin-top: calc(($font-15px - $font-13px) / 2); /* To align with the name */
|
||||||
|
font-size: $font-13px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_wrapper {
|
||||||
|
height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
margin: 0 20px 0 16px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-bottom: 1px solid $separator;
|
||||||
|
|
||||||
|
.mx_InviteOnlyIcon_large {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BetaCard_betaPill {
|
||||||
|
margin-right: $spacing-8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_name {
|
||||||
|
flex: 0 1 auto;
|
||||||
|
overflow: hidden;
|
||||||
|
color: $primary-content;
|
||||||
|
font: var(--cpd-font-heading-sm-semibold);
|
||||||
|
font-weight: var(--cpd-font-weight-semibold);
|
||||||
|
min-height: 24px;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0 3px;
|
||||||
|
padding: 1px 4px;
|
||||||
|
display: flex;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $quinary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_nametext {
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_chevron {
|
||||||
|
align-self: center;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
|
||||||
|
background-color: $tertiary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_LegacyRoomHeader_name--textonly {
|
||||||
|
cursor: unset;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[aria-expanded="true"] {
|
||||||
|
background-color: $separator;
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_chevron {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_settingsHint {
|
||||||
|
color: $settings-grey-fg-color !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_searchStatus {
|
||||||
|
font-weight: normal;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RoomTopic {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_topic {
|
||||||
|
$lines: 2;
|
||||||
|
|
||||||
|
flex: 1;
|
||||||
|
color: $secondary-content;
|
||||||
|
font: var(--cpd-font-body-sm-regular);
|
||||||
|
line-height: 1rem;
|
||||||
|
max-height: calc(1rem * $lines);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-line-clamp: $lines; /* See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
display: -webkit-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_topic .mx_Emoji {
|
||||||
|
/* Undo font size increase to prevent vertical cropping and ensure the same size */
|
||||||
|
/* as in plain text emojis */
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_avatar {
|
||||||
|
flex: 0;
|
||||||
|
margin: 0 7px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_avatar .mx_BaseAvatar_image {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_button {
|
||||||
|
cursor: pointer;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
margin-left: 1px;
|
||||||
|
margin-right: 1px;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
position: relative;
|
||||||
|
border-radius: 100%;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 4px; /* center with parent of 32px */
|
||||||
|
left: 4px; /* center with parent of 32px */
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
background-color: $icon-button-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $accent-300;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_button_unreadIndicator_bg {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--RoomHeader-indicator-dot-offset);
|
||||||
|
top: var(--RoomHeader-indicator-dot-offset);
|
||||||
|
margin: 4px;
|
||||||
|
width: var(--RoomHeader-indicator-dot-size);
|
||||||
|
height: var(--RoomHeader-indicator-dot-size);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(1.6);
|
||||||
|
transform-origin: center center;
|
||||||
|
background: $background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_button_unreadIndicator {
|
||||||
|
position: absolute;
|
||||||
|
right: var(--RoomHeader-indicator-dot-offset);
|
||||||
|
top: var(--RoomHeader-indicator-dot-offset);
|
||||||
|
margin: 4px;
|
||||||
|
|
||||||
|
&.mx_Indicator_red {
|
||||||
|
background: $alert;
|
||||||
|
box-shadow: $alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_Indicator_gray {
|
||||||
|
background: $room-icon-unread-color;
|
||||||
|
box-shadow: $room-icon-unread-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_Indicator_bold {
|
||||||
|
background: $primary-content;
|
||||||
|
box-shadow: $primary-content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_button--unread {
|
||||||
|
&::before {
|
||||||
|
background-color: $room-icon-unread-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_button--highlight,
|
||||||
|
.mx_LegacyRoomHeader_button:hover {
|
||||||
|
&::before {
|
||||||
|
background-color: $accent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_forgetButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/leave.svg");
|
||||||
|
width: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_appsButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/room/apps.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_appsButton_highlight::before {
|
||||||
|
background-color: $accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_searchButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/room/search-inset.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_inviteButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/room/invite.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_voiceCallButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/voice-call.svg");
|
||||||
|
|
||||||
|
/* The call button SVG is padded slightly differently, so match it up to the size */
|
||||||
|
/* of the other icons */
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_videoCallButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_layoutButton--freedom::before,
|
||||||
|
.mx_LegacyRoomHeader_freedomIcon::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/freedom.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_layoutButton--spotlight::before,
|
||||||
|
.mx_LegacyRoomHeader_spotlightIcon::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/call/spotlight.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_closeButton {
|
||||||
|
&::before {
|
||||||
|
mask-image: url("$(res)/img/cancel.svg");
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: unset; /* remove background color on hover */
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $icon-button-color; /* set the default background color */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_minimiseButton::before {
|
||||||
|
mask-image: url("$(res)/img/element-icons/reduce.svg");
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader_layoutMenu .mx_IconizedContextMenu_icon::before {
|
||||||
|
content: "";
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 20px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background: $primary-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 480px) {
|
||||||
|
.mx_LegacyRoomHeader_wrapper {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LegacyRoomHeader {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,36 +20,13 @@ limitations under the License.
|
||||||
--RoomHeader-indicator-pulseColor: $alert;
|
--RoomHeader-indicator-pulseColor: $alert;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader {
|
.mx_LegacyRoomHeader {
|
||||||
flex: 0 0 50px;
|
flex: 0 0 50px;
|
||||||
border-bottom: 1px solid $primary-hairline-color;
|
border-bottom: 1px solid $primary-hairline-color;
|
||||||
background-color: $background;
|
background-color: $background;
|
||||||
|
|
||||||
.mx_RoomHeader_icon {
|
|
||||||
height: 12px;
|
|
||||||
width: 12px;
|
|
||||||
|
|
||||||
&.mx_RoomHeader_icon_video {
|
|
||||||
height: 14px;
|
|
||||||
width: 14px;
|
|
||||||
background-color: $secondary-content;
|
|
||||||
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
|
||||||
mask-size: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_E2EIcon {
|
|
||||||
margin: 0;
|
|
||||||
height: 100%; /* To give the tooltip room to breathe */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CallDuration {
|
|
||||||
margin-top: calc(($font-15px - $font-13px) / 2); /* To align with the name */
|
|
||||||
font-size: $font-13px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_wrapper {
|
.mx_LegacyRoomHeader_wrapper {
|
||||||
height: 44px;
|
height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -57,17 +34,9 @@ limitations under the License.
|
||||||
margin: 0 20px 0 16px;
|
margin: 0 20px 0 16px;
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
border-bottom: 1px solid $separator;
|
border-bottom: 1px solid $separator;
|
||||||
|
|
||||||
.mx_InviteOnlyIcon_large {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BetaCard_betaPill {
|
|
||||||
margin-right: $spacing-8;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_name {
|
.mx_LegacyRoomHeader_name {
|
||||||
flex: 0 1 auto;
|
flex: 0 1 auto;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: $primary-content;
|
color: $primary-content;
|
||||||
|
@ -81,252 +50,4 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: $quinary-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_nametext {
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_chevron {
|
|
||||||
align-self: center;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 20px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
|
|
||||||
background-color: $tertiary-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_RoomHeader_name--textonly {
|
|
||||||
cursor: unset;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[aria-expanded="true"] {
|
|
||||||
background-color: $separator;
|
|
||||||
|
|
||||||
.mx_RoomHeader_chevron {
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_settingsHint {
|
|
||||||
color: $settings-grey-fg-color !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_searchStatus {
|
|
||||||
font-weight: normal;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomTopic {
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_topic {
|
|
||||||
$lines: 2;
|
|
||||||
|
|
||||||
flex: 1;
|
|
||||||
color: $secondary-content;
|
|
||||||
font: var(--cpd-font-body-sm-regular);
|
|
||||||
line-height: 1rem;
|
|
||||||
max-height: calc(1rem * $lines);
|
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
-webkit-line-clamp: $lines; /* See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp */
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
display: -webkit-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_topic .mx_Emoji {
|
|
||||||
/* Undo font size increase to prevent vertical cropping and ensure the same size */
|
|
||||||
/* as in plain text emojis */
|
|
||||||
font-size: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_avatar {
|
|
||||||
flex: 0;
|
|
||||||
margin: 0 7px;
|
|
||||||
position: relative;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_avatar .mx_BaseAvatar_image {
|
|
||||||
object-fit: cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_button {
|
|
||||||
cursor: pointer;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin-left: 1px;
|
|
||||||
margin-right: 1px;
|
|
||||||
height: 32px;
|
|
||||||
width: 32px;
|
|
||||||
position: relative;
|
|
||||||
border-radius: 100%;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 4px; /* center with parent of 32px */
|
|
||||||
left: 4px; /* center with parent of 32px */
|
|
||||||
height: 24px;
|
|
||||||
width: 24px;
|
|
||||||
background-color: $icon-button-color;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-size: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: $accent-300;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $accent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_button_unreadIndicator_bg {
|
|
||||||
position: absolute;
|
|
||||||
right: var(--RoomHeader-indicator-dot-offset);
|
|
||||||
top: var(--RoomHeader-indicator-dot-offset);
|
|
||||||
margin: 4px;
|
|
||||||
width: var(--RoomHeader-indicator-dot-size);
|
|
||||||
height: var(--RoomHeader-indicator-dot-size);
|
|
||||||
border-radius: 50%;
|
|
||||||
transform: scale(1.6);
|
|
||||||
transform-origin: center center;
|
|
||||||
background: $background;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_button_unreadIndicator {
|
|
||||||
position: absolute;
|
|
||||||
right: var(--RoomHeader-indicator-dot-offset);
|
|
||||||
top: var(--RoomHeader-indicator-dot-offset);
|
|
||||||
margin: 4px;
|
|
||||||
|
|
||||||
&.mx_Indicator_red {
|
|
||||||
background: $alert;
|
|
||||||
box-shadow: $alert;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_Indicator_gray {
|
|
||||||
background: $room-icon-unread-color;
|
|
||||||
box-shadow: $room-icon-unread-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_Indicator_bold {
|
|
||||||
background: $primary-content;
|
|
||||||
box-shadow: $primary-content;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_button--unread {
|
|
||||||
&::before {
|
|
||||||
background-color: $room-icon-unread-color !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_button--highlight,
|
|
||||||
.mx_RoomHeader_button:hover {
|
|
||||||
&::before {
|
|
||||||
background-color: $accent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_forgetButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/leave.svg");
|
|
||||||
width: 26px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_appsButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/room/apps.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_appsButton_highlight::before {
|
|
||||||
background-color: $accent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_searchButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/room/search-inset.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_inviteButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/room/invite.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_voiceCallButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/call/voice-call.svg");
|
|
||||||
|
|
||||||
/* The call button SVG is padded slightly differently, so match it up to the size */
|
|
||||||
/* of the other icons */
|
|
||||||
mask-size: 20px;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_videoCallButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_layoutButton--freedom::before,
|
|
||||||
.mx_RoomHeader_freedomIcon::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/call/freedom.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_layoutButton--spotlight::before,
|
|
||||||
.mx_RoomHeader_spotlightIcon::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/call/spotlight.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_closeButton {
|
|
||||||
&::before {
|
|
||||||
mask-image: url("$(res)/img/cancel.svg");
|
|
||||||
mask-size: 20px;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: unset; /* remove background color on hover */
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $icon-button-color; /* set the default background color */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_minimiseButton::before {
|
|
||||||
mask-image: url("$(res)/img/element-icons/reduce.svg");
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader_layoutMenu .mx_IconizedContextMenu_icon::before {
|
|
||||||
content: "";
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
display: block;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 20px;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
background: $primary-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 480px) {
|
|
||||||
.mx_RoomHeader_wrapper {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomHeader {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,8 @@ import RoomPreviewCard from "../views/rooms/RoomPreviewCard";
|
||||||
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader, { ISearchInfo } from "../views/rooms/RoomHeader";
|
import LegacyRoomHeader, { ISearchInfo } from "../views/rooms/LegacyRoomHeader";
|
||||||
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
import { IOOBData, IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||||
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
import EffectsOverlay from "../views/elements/EffectsOverlay";
|
||||||
import { containsEmoji } from "../../effects/utils";
|
import { containsEmoji } from "../../effects/utils";
|
||||||
|
@ -295,22 +296,26 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView mx_RoomView--local">
|
<div className="mx_RoomView mx_RoomView--local">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||||
room={context.room}
|
<RoomHeader room={context.room} />
|
||||||
searchInfo={undefined}
|
) : (
|
||||||
inRoom={true}
|
<LegacyRoomHeader
|
||||||
onSearchClick={null}
|
room={context.room}
|
||||||
onInviteClick={null}
|
searchInfo={undefined}
|
||||||
onForgetClick={null}
|
inRoom={true}
|
||||||
e2eStatus={room.encrypted ? E2EStatus.Normal : undefined}
|
onSearchClick={null}
|
||||||
onAppsClick={null}
|
onInviteClick={null}
|
||||||
appsShown={false}
|
onForgetClick={null}
|
||||||
excludedRightPanelPhaseButtons={[]}
|
e2eStatus={room.encrypted ? E2EStatus.Normal : undefined}
|
||||||
showButtons={false}
|
onAppsClick={null}
|
||||||
enableRoomOptionsMenu={false}
|
appsShown={false}
|
||||||
viewingCall={false}
|
excludedRightPanelPhaseButtons={[]}
|
||||||
activeCall={null}
|
showButtons={false}
|
||||||
/>
|
enableRoomOptionsMenu={false}
|
||||||
|
viewingCall={false}
|
||||||
|
activeCall={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<main className="mx_RoomView_body" ref={props.roomView}>
|
<main className="mx_RoomView_body" ref={props.roomView}>
|
||||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
|
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
|
||||||
<div className="mx_RoomView_timeline">
|
<div className="mx_RoomView_timeline">
|
||||||
|
@ -345,22 +350,26 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView mx_RoomView--local">
|
<div className="mx_RoomView mx_RoomView--local">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||||
room={context.room}
|
<RoomHeader room={context.room} />
|
||||||
searchInfo={undefined}
|
) : (
|
||||||
inRoom={true}
|
<LegacyRoomHeader
|
||||||
onSearchClick={null}
|
room={context.room}
|
||||||
onInviteClick={null}
|
searchInfo={undefined}
|
||||||
onForgetClick={null}
|
inRoom={true}
|
||||||
e2eStatus={props.localRoom.encrypted ? E2EStatus.Normal : undefined}
|
onSearchClick={null}
|
||||||
onAppsClick={null}
|
onInviteClick={null}
|
||||||
appsShown={false}
|
onForgetClick={null}
|
||||||
excludedRightPanelPhaseButtons={[]}
|
e2eStatus={props.localRoom.encrypted ? E2EStatus.Normal : undefined}
|
||||||
showButtons={false}
|
onAppsClick={null}
|
||||||
enableRoomOptionsMenu={false}
|
appsShown={false}
|
||||||
viewingCall={false}
|
excludedRightPanelPhaseButtons={[]}
|
||||||
activeCall={null}
|
showButtons={false}
|
||||||
/>
|
enableRoomOptionsMenu={false}
|
||||||
|
viewingCall={false}
|
||||||
|
activeCall={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="mx_RoomView_body">
|
<div className="mx_RoomView_body">
|
||||||
<LargeLoader text={text} />
|
<LargeLoader text={text} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -2460,23 +2469,27 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||||
)}
|
)}
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||||
room={this.state.room}
|
<RoomHeader room={this.state.room} />
|
||||||
searchInfo={this.state.search}
|
) : (
|
||||||
oobData={this.props.oobData}
|
<LegacyRoomHeader
|
||||||
inRoom={myMembership === "join"}
|
room={this.state.room}
|
||||||
onSearchClick={onSearchClick}
|
searchInfo={this.state.search}
|
||||||
onInviteClick={onInviteClick}
|
oobData={this.props.oobData}
|
||||||
onForgetClick={showForgetButton ? onForgetClick : null}
|
inRoom={myMembership === "join"}
|
||||||
e2eStatus={this.state.e2eStatus}
|
onSearchClick={onSearchClick}
|
||||||
onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null}
|
onInviteClick={onInviteClick}
|
||||||
appsShown={this.state.showApps}
|
onForgetClick={showForgetButton ? onForgetClick : null}
|
||||||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
e2eStatus={this.state.e2eStatus}
|
||||||
showButtons={!this.viewsLocalRoom}
|
onAppsClick={this.state.hasPinnedWidgets ? onAppsClick : null}
|
||||||
enableRoomOptionsMenu={!this.viewsLocalRoom}
|
appsShown={this.state.showApps}
|
||||||
viewingCall={viewingCall}
|
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||||
activeCall={this.state.activeCall}
|
showButtons={!this.viewsLocalRoom}
|
||||||
/>
|
enableRoomOptionsMenu={!this.viewsLocalRoom}
|
||||||
|
viewingCall={viewingCall}
|
||||||
|
activeCall={this.state.activeCall}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MainSplit
|
<MainSplit
|
||||||
panel={rightPanel}
|
panel={rightPanel}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { useRoomContext } from "../../contexts/RoomContext";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import { E2EStatus } from "../../utils/ShieldUtils";
|
import { E2EStatus } from "../../utils/ShieldUtils";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
|
import LegacyRoomHeader from "../views/rooms/LegacyRoomHeader";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
import ScrollPanel from "./ScrollPanel";
|
import ScrollPanel from "./ScrollPanel";
|
||||||
import EventTileBubble from "../views/messages/EventTileBubble";
|
import EventTileBubble from "../views/messages/EventTileBubble";
|
||||||
|
@ -29,6 +30,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro";
|
||||||
import { UnwrappedEventTile } from "../views/rooms/EventTile";
|
import { UnwrappedEventTile } from "../views/rooms/EventTile";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
roomView: RefObject<HTMLElement>;
|
roomView: RefObject<HTMLElement>;
|
||||||
|
@ -48,21 +50,25 @@ export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resize
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomView mx_RoomView--local">
|
<div className="mx_RoomView mx_RoomView--local">
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
<RoomHeader
|
{SettingsStore.getValue("feature_new_room_decoration_ui") ? (
|
||||||
room={context.room}
|
<RoomHeader room={context.room} />
|
||||||
inRoom={true}
|
) : (
|
||||||
onSearchClick={null}
|
<LegacyRoomHeader
|
||||||
onInviteClick={null}
|
room={context.room}
|
||||||
onForgetClick={null}
|
inRoom={true}
|
||||||
e2eStatus={E2EStatus.Normal}
|
onSearchClick={null}
|
||||||
onAppsClick={null}
|
onInviteClick={null}
|
||||||
appsShown={false}
|
onForgetClick={null}
|
||||||
excludedRightPanelPhaseButtons={[]}
|
e2eStatus={E2EStatus.Normal}
|
||||||
showButtons={false}
|
onAppsClick={null}
|
||||||
enableRoomOptionsMenu={false}
|
appsShown={false}
|
||||||
viewingCall={false}
|
excludedRightPanelPhaseButtons={[]}
|
||||||
activeCall={null}
|
showButtons={false}
|
||||||
/>
|
enableRoomOptionsMenu={false}
|
||||||
|
viewingCall={false}
|
||||||
|
activeCall={null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<main className="mx_RoomView_body" ref={roomView}>
|
<main className="mx_RoomView_body" ref={roomView}>
|
||||||
<div className="mx_RoomView_timeline">
|
<div className="mx_RoomView_timeline">
|
||||||
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={resizeNotifier}>
|
<ScrollPanel className="mx_RoomView_messagePanel" resizeNotifier={resizeNotifier}>
|
||||||
|
|
|
@ -59,6 +59,7 @@ interface IProps extends IContextMenuProps {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room context menu accessible via the room header.
|
* Room context menu accessible via the room header.
|
||||||
|
* @deprecated will be removed as part of `feature_new_room_decoration_ui`
|
||||||
*/
|
*/
|
||||||
const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
|
const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
|
@ -45,9 +45,9 @@ export default class HeaderButton extends React.Component<IProps> {
|
||||||
const { isHighlighted, isUnread = false, onClick, name, title, ...props } = this.props;
|
const { isHighlighted, isUnread = false, onClick, name, title, ...props } = this.props;
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_RoomHeader_button": true,
|
"mx_LegacyRoomHeader_button": true,
|
||||||
"mx_RoomHeader_button--highlight": isHighlighted,
|
"mx_LegacyRoomHeader_button--highlight": isHighlighted,
|
||||||
"mx_RoomHeader_button--unread": isUnread,
|
"mx_LegacyRoomHeader_button--unread": isUnread,
|
||||||
[`mx_RightPanel_${name}`]: true,
|
[`mx_RightPanel_${name}`]: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -64,14 +64,14 @@ const UnreadIndicator: React.FC<IUnreadIndicatorProps> = ({ color }) => {
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
mx_Indicator: true,
|
mx_Indicator: true,
|
||||||
mx_RoomHeader_button_unreadIndicator: true,
|
mx_LegacyRoomHeader_button_unreadIndicator: true,
|
||||||
mx_Indicator_bold: color === NotificationColor.Bold,
|
mx_Indicator_bold: color === NotificationColor.Bold,
|
||||||
mx_Indicator_gray: color === NotificationColor.Grey,
|
mx_Indicator_gray: color === NotificationColor.Grey,
|
||||||
mx_Indicator_red: color === NotificationColor.Red,
|
mx_Indicator_red: color === NotificationColor.Red,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mx_RoomHeader_button_unreadIndicator_bg" />
|
<div className="mx_LegacyRoomHeader_button_unreadIndicator_bg" />
|
||||||
<div className={classes} />
|
<div className={classes} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -127,7 +127,10 @@ interface IProps {
|
||||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
/**
|
||||||
|
* @deprecated will be removed as part of 'feature_new_room_decoration_ui'
|
||||||
|
*/
|
||||||
|
export default class LegacyRoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView];
|
private static readonly THREAD_PHASES = [RightPanelPhases.ThreadPanel, RightPanelPhases.ThreadView];
|
||||||
private globalNotificationState: SummarizedNotificationState;
|
private globalNotificationState: SummarizedNotificationState;
|
||||||
|
|
||||||
|
@ -257,7 +260,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
};
|
};
|
||||||
|
|
||||||
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
private onThreadsPanelClicked = (ev: ButtonEvent): void => {
|
||||||
if (this.state.phase && RoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
|
if (this.state.phase && LegacyRoomHeaderButtons.THREAD_PHASES.includes(this.state.phase)) {
|
||||||
RightPanelStore.instance.togglePanel(this.props.room?.roomId ?? null);
|
RightPanelStore.instance.togglePanel(this.props.room?.roomId ?? null);
|
||||||
} else {
|
} else {
|
||||||
showThreadPanel();
|
showThreadPanel();
|
||||||
|
@ -300,7 +303,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
data-testid="threadsButton"
|
data-testid="threadsButton"
|
||||||
title={_t("Threads")}
|
title={_t("Threads")}
|
||||||
onClick={this.onThreadsPanelClicked}
|
onClick={this.onThreadsPanelClicked}
|
||||||
isHighlighted={this.isPhase(RoomHeaderButtons.THREAD_PHASES)}
|
isHighlighted={this.isPhase(LegacyRoomHeaderButtons.THREAD_PHASES)}
|
||||||
isUnread={this.state.threadNotificationColor > NotificationColor.None}
|
isUnread={this.state.threadNotificationColor > NotificationColor.None}
|
||||||
>
|
>
|
||||||
<UnreadIndicator color={this.state.threadNotificationColor} />
|
<UnreadIndicator color={this.state.threadNotificationColor} />
|
825
src/components/views/rooms/LegacyRoomHeader.tsx
Normal file
825
src/components/views/rooms/LegacyRoomHeader.tsx
Normal file
|
@ -0,0 +1,825 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2019, 2021 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, { FC, useState, useMemo, useCallback } from "react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { throttle } from "lodash";
|
||||||
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||||
|
|
||||||
|
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
import { UserTab } from "../dialogs/UserTab";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import RoomHeaderButtons from "../right_panel/LegacyRoomHeaderButtons";
|
||||||
|
import E2EIcon from "./E2EIcon";
|
||||||
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
|
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
|
import RoomTopic from "../elements/RoomTopic";
|
||||||
|
import RoomName from "../elements/RoomName";
|
||||||
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
|
import { SearchScope } from "./SearchBar";
|
||||||
|
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
|
import RoomContextMenu from "../context_menus/RoomContextMenu";
|
||||||
|
import { contextMenuBelow } from "./RoomTile";
|
||||||
|
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
||||||
|
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
import RoomLiveShareWarning from "../beacon/RoomLiveShareWarning";
|
||||||
|
import { BetaPill } from "../beta/BetaCard";
|
||||||
|
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
||||||
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
|
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
|
||||||
|
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
|
||||||
|
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
||||||
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||||
|
import { useWidgets } from "../right_panel/RoomSummaryCard";
|
||||||
|
import { WidgetType } from "../../../widgets/WidgetType";
|
||||||
|
import { useCall, useLayout } from "../../../hooks/useCall";
|
||||||
|
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||||
|
import { Call, ElementCall, Layout } from "../../../models/Call";
|
||||||
|
import IconizedContextMenu, {
|
||||||
|
IconizedContextMenuOption,
|
||||||
|
IconizedContextMenuOptionList,
|
||||||
|
IconizedContextMenuRadio,
|
||||||
|
} from "../context_menus/IconizedContextMenu";
|
||||||
|
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
|
import { GroupCallDuration } from "../voip/CallDuration";
|
||||||
|
import { Alignment } from "../elements/Tooltip";
|
||||||
|
import RoomCallBanner from "../beacon/RoomCallBanner";
|
||||||
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
|
import { UIComponent } from "../../../settings/UIFeature";
|
||||||
|
|
||||||
|
class DisabledWithReason {
|
||||||
|
public constructor(public readonly reason: string) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VoiceCallButtonProps {
|
||||||
|
room: Room;
|
||||||
|
busy: boolean;
|
||||||
|
setBusy: (value: boolean) => void;
|
||||||
|
behavior: DisabledWithReason | "legacy_or_jitsi";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button for starting voice calls, supporting only legacy 1:1 calls and Jitsi
|
||||||
|
* widgets.
|
||||||
|
*/
|
||||||
|
const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavior }) => {
|
||||||
|
const { onClick, tooltip, disabled } = useMemo(() => {
|
||||||
|
if (behavior instanceof DisabledWithReason) {
|
||||||
|
return {
|
||||||
|
onClick: () => {},
|
||||||
|
tooltip: behavior.reason,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// behavior === "legacy_or_jitsi"
|
||||||
|
return {
|
||||||
|
onClick: async (ev: ButtonEvent): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
setBusy(true);
|
||||||
|
await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Voice);
|
||||||
|
setBusy(false);
|
||||||
|
},
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [behavior, room, setBusy]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_voiceCallButton"
|
||||||
|
onClick={onClick}
|
||||||
|
title={_t("Voice call")}
|
||||||
|
tooltip={tooltip ?? _t("Voice call")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
disabled={disabled || busy}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface VideoCallButtonProps {
|
||||||
|
room: Room;
|
||||||
|
busy: boolean;
|
||||||
|
setBusy: (value: boolean) => void;
|
||||||
|
behavior: DisabledWithReason | "legacy_or_jitsi" | "element" | "jitsi_or_element";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Button for starting video calls, supporting both legacy 1:1 calls, Jitsi
|
||||||
|
* widgets, and native group calls. If multiple calling options are available,
|
||||||
|
* this shows a menu to pick between them.
|
||||||
|
*/
|
||||||
|
const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavior }) => {
|
||||||
|
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
const startLegacyCall = useCallback(async (): Promise<void> => {
|
||||||
|
setBusy(true);
|
||||||
|
await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Video);
|
||||||
|
setBusy(false);
|
||||||
|
}, [setBusy, room]);
|
||||||
|
|
||||||
|
const startElementCall = useCallback(() => {
|
||||||
|
setBusy(true);
|
||||||
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: true,
|
||||||
|
metricsTrigger: undefined,
|
||||||
|
});
|
||||||
|
setBusy(false);
|
||||||
|
}, [setBusy, room]);
|
||||||
|
|
||||||
|
const { onClick, tooltip, disabled } = useMemo(() => {
|
||||||
|
if (behavior instanceof DisabledWithReason) {
|
||||||
|
return {
|
||||||
|
onClick: () => {},
|
||||||
|
tooltip: behavior.reason,
|
||||||
|
disabled: true,
|
||||||
|
};
|
||||||
|
} else if (behavior === "legacy_or_jitsi") {
|
||||||
|
return {
|
||||||
|
onClick: async (ev: ButtonEvent): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
await startLegacyCall();
|
||||||
|
},
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
} else if (behavior === "element") {
|
||||||
|
return {
|
||||||
|
onClick: async (ev: ButtonEvent): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
startElementCall();
|
||||||
|
},
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// behavior === "jitsi_or_element"
|
||||||
|
return {
|
||||||
|
onClick: async (ev: ButtonEvent): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
openMenu();
|
||||||
|
},
|
||||||
|
disabled: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [behavior, startLegacyCall, startElementCall, openMenu]);
|
||||||
|
|
||||||
|
const onJitsiClick = useCallback(
|
||||||
|
async (ev: ButtonEvent): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
closeMenu();
|
||||||
|
await startLegacyCall();
|
||||||
|
},
|
||||||
|
[closeMenu, startLegacyCall],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onElementClick = useCallback(
|
||||||
|
(ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
closeMenu();
|
||||||
|
startElementCall();
|
||||||
|
},
|
||||||
|
[closeMenu, startElementCall],
|
||||||
|
);
|
||||||
|
|
||||||
|
let menu: JSX.Element | null = null;
|
||||||
|
if (menuOpen) {
|
||||||
|
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||||
|
const brand = SdkConfig.get("element_call").brand;
|
||||||
|
menu = (
|
||||||
|
<IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||||
|
<IconizedContextMenuOption
|
||||||
|
label={_t("Video call (%(brand)s)", { brand })}
|
||||||
|
onClick={onElementClick}
|
||||||
|
/>
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
inputRef={buttonRef}
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_videoCallButton"
|
||||||
|
onClick={onClick}
|
||||||
|
title={_t("Video call")}
|
||||||
|
tooltip={tooltip ?? _t("Video call")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
disabled={disabled || busy}
|
||||||
|
/>
|
||||||
|
{menu}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CallButtonsProps {
|
||||||
|
room: Room;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The header buttons for placing calls have become stupidly complex, so here
|
||||||
|
// they are as a separate component
|
||||||
|
const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
const showButtons = useSettingValue<boolean>("showCallButtonsInComposer");
|
||||||
|
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||||
|
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||||
|
const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, room]);
|
||||||
|
const useElementCallExclusively = useMemo(() => {
|
||||||
|
return SdkConfig.get("element_call").use_exclusively;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const hasLegacyCall = useEventEmitterState(
|
||||||
|
LegacyCallHandler.instance,
|
||||||
|
LegacyCallHandlerEvent.CallsChanged,
|
||||||
|
useCallback(() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null, [room]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const widgets = useWidgets(room);
|
||||||
|
const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
||||||
|
|
||||||
|
const hasGroupCall = useCall(room.roomId) !== null;
|
||||||
|
|
||||||
|
const [functionalMembers, mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState(
|
||||||
|
room,
|
||||||
|
RoomStateEvent.Update,
|
||||||
|
useCallback(
|
||||||
|
() => [
|
||||||
|
getJoinedNonFunctionalMembers(room),
|
||||||
|
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
||||||
|
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
|
||||||
|
],
|
||||||
|
[room],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element => (
|
||||||
|
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
|
||||||
|
);
|
||||||
|
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element => (
|
||||||
|
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isVideoRoom || !showButtons) {
|
||||||
|
return null;
|
||||||
|
} else if (groupCallsEnabled) {
|
||||||
|
if (useElementCallExclusively) {
|
||||||
|
if (hasGroupCall) {
|
||||||
|
return makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")));
|
||||||
|
} else if (mayCreateElementCalls) {
|
||||||
|
return makeVideoCallButton("element");
|
||||||
|
} else {
|
||||||
|
return makeVideoCallButton(
|
||||||
|
new DisabledWithReason(_t("You do not have permission to start video calls")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||||
|
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (functionalMembers.length <= 1) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||||
|
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (functionalMembers.length === 2) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||||
|
{makeVideoCallButton("legacy_or_jitsi")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (mayEditWidgets) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||||
|
{makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const videoCallBehavior = mayCreateElementCalls
|
||||||
|
? "element"
|
||||||
|
: new DisabledWithReason(_t("You do not have permission to start video calls"));
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||||
|
{makeVideoCallButton(videoCallBehavior)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (hasLegacyCall || hasJitsiWidget) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||||
|
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (functionalMembers.length <= 1) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||||
|
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else if (functionalMembers.length === 2 || mayEditWidgets) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton("legacy_or_jitsi")}
|
||||||
|
{makeVideoCallButton("legacy_or_jitsi")}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
||||||
|
{makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls")))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CallLayoutSelectorProps {
|
||||||
|
call: ElementCall;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
||||||
|
const layout = useLayout(call);
|
||||||
|
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
||||||
|
|
||||||
|
const onClick = useCallback(
|
||||||
|
(ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
openMenu();
|
||||||
|
},
|
||||||
|
[openMenu],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFreedomClick = useCallback(
|
||||||
|
(ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
closeMenu();
|
||||||
|
call.setLayout(Layout.Tile);
|
||||||
|
},
|
||||||
|
[closeMenu, call],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSpotlightClick = useCallback(
|
||||||
|
(ev: ButtonEvent) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
closeMenu();
|
||||||
|
call.setLayout(Layout.Spotlight);
|
||||||
|
},
|
||||||
|
[closeMenu, call],
|
||||||
|
);
|
||||||
|
|
||||||
|
let menu: JSX.Element | null = null;
|
||||||
|
if (menuOpen) {
|
||||||
|
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||||
|
menu = (
|
||||||
|
<IconizedContextMenu
|
||||||
|
className="mx_LegacyRoomHeader_layoutMenu"
|
||||||
|
{...aboveLeftOf(buttonRect)}
|
||||||
|
onFinished={closeMenu}
|
||||||
|
>
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
<IconizedContextMenuRadio
|
||||||
|
iconClassName="mx_LegacyRoomHeader_freedomIcon"
|
||||||
|
label={_t("Freedom")}
|
||||||
|
active={layout === Layout.Tile}
|
||||||
|
onClick={onFreedomClick}
|
||||||
|
/>
|
||||||
|
<IconizedContextMenuRadio
|
||||||
|
iconClassName="mx_LegacyRoomHeader_spotlightIcon"
|
||||||
|
label={_t("Spotlight")}
|
||||||
|
active={layout === Layout.Spotlight}
|
||||||
|
onClick={onSpotlightClick}
|
||||||
|
/>
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
</IconizedContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
inputRef={buttonRef}
|
||||||
|
className={classNames("mx_LegacyRoomHeader_button", {
|
||||||
|
"mx_LegacyRoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
||||||
|
"mx_LegacyRoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
||||||
|
})}
|
||||||
|
onClick={onClick}
|
||||||
|
title={_t("Change layout")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
key="layout"
|
||||||
|
/>
|
||||||
|
{menu}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ISearchInfo {
|
||||||
|
searchId: number;
|
||||||
|
roomId?: string;
|
||||||
|
term: string;
|
||||||
|
scope: SearchScope;
|
||||||
|
promise: Promise<ISearchResults>;
|
||||||
|
abortController?: AbortController;
|
||||||
|
|
||||||
|
inProgress?: boolean;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProps {
|
||||||
|
room: Room;
|
||||||
|
oobData?: IOOBData;
|
||||||
|
inRoom: boolean;
|
||||||
|
onSearchClick: (() => void) | null;
|
||||||
|
onInviteClick: (() => void) | null;
|
||||||
|
onForgetClick: (() => void) | null;
|
||||||
|
onAppsClick: (() => void) | null;
|
||||||
|
e2eStatus: E2EStatus;
|
||||||
|
appsShown: boolean;
|
||||||
|
searchInfo?: ISearchInfo;
|
||||||
|
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||||
|
showButtons?: boolean;
|
||||||
|
enableRoomOptionsMenu?: boolean;
|
||||||
|
viewingCall: boolean;
|
||||||
|
activeCall: Call | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
contextMenuPosition?: DOMRect;
|
||||||
|
rightPanelOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps: Partial<IProps> = {
|
||||||
|
inRoom: false,
|
||||||
|
excludedRightPanelPhaseButtons: [],
|
||||||
|
showButtons: true,
|
||||||
|
enableRoomOptionsMenu: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static contextType = RoomContext;
|
||||||
|
public context!: React.ContextType<typeof RoomContext>;
|
||||||
|
private readonly client = this.props.room.client;
|
||||||
|
|
||||||
|
public constructor(props: IProps, context: IState) {
|
||||||
|
super(props, context);
|
||||||
|
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
|
||||||
|
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
|
this.state = {
|
||||||
|
rightPanelOpen: RightPanelStore.instance.isOpen,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
|
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||||
|
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||||
|
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
||||||
|
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
|
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onRightPanelStoreUpdate = (): void => {
|
||||||
|
this.setState({ rightPanelOpen: RightPanelStore.instance.isOpen });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onRoomStateEvents = (event: MatrixEvent): void => {
|
||||||
|
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// redisplay the room name, topic, etc.
|
||||||
|
this.rateLimitedUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
private onNotificationUpdate = (): void => {
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
private rateLimitedUpdate = throttle(
|
||||||
|
() => {
|
||||||
|
this.forceUpdate();
|
||||||
|
},
|
||||||
|
500,
|
||||||
|
{ leading: true, trailing: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
private onContextMenuOpenClick = (ev: ButtonEvent): void => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
const target = ev.target as HTMLButtonElement;
|
||||||
|
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onContextMenuCloseClick = (): void => {
|
||||||
|
this.setState({ contextMenuPosition: undefined });
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHideCallClick = (ev: ButtonEvent): void => {
|
||||||
|
ev.preventDefault();
|
||||||
|
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: this.props.room.roomId,
|
||||||
|
view_call: false,
|
||||||
|
metricsTrigger: undefined,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
private renderButtons(isVideoRoom: boolean): React.ReactNode {
|
||||||
|
const startButtons: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) {
|
||||||
|
startButtons.push(<CallButtons key="calls" room={this.props.room} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
|
||||||
|
startButtons.push(<CallLayoutSelector key="layout" call={this.props.activeCall} />);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.viewingCall && this.props.onForgetClick) {
|
||||||
|
startButtons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_forgetButton"
|
||||||
|
onClick={this.props.onForgetClick}
|
||||||
|
title={_t("Forget room")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
key="forget"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.viewingCall && this.props.onAppsClick) {
|
||||||
|
startButtons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={classNames("mx_LegacyRoomHeader_button mx_LegacyRoomHeader_appsButton", {
|
||||||
|
mx_LegacyRoomHeader_appsButton_highlight: this.props.appsShown,
|
||||||
|
})}
|
||||||
|
onClick={this.props.onAppsClick}
|
||||||
|
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
||||||
|
aria-checked={this.props.appsShown}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
key="apps"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
||||||
|
startButtons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_searchButton"
|
||||||
|
onClick={this.props.onSearchClick}
|
||||||
|
title={_t("Search")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
key="search"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
||||||
|
startButtons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_inviteButton"
|
||||||
|
onClick={this.props.onInviteClick}
|
||||||
|
title={_t("Invite")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
key="invite"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const endButtons: JSX.Element[] = [];
|
||||||
|
|
||||||
|
if (this.props.viewingCall && !isVideoRoom) {
|
||||||
|
if (this.props.activeCall === null) {
|
||||||
|
endButtons.push(
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_closeButton"
|
||||||
|
onClick={this.onHideCallClick}
|
||||||
|
title={_t("Close call")}
|
||||||
|
key="close"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
endButtons.push(
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className="mx_LegacyRoomHeader_button mx_LegacyRoomHeader_minimiseButton"
|
||||||
|
onClick={this.onHideCallClick}
|
||||||
|
title={_t("View chat timeline")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
key="minimise"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{startButtons}
|
||||||
|
<RoomHeaderButtons
|
||||||
|
room={this.props.room}
|
||||||
|
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
|
||||||
|
/>
|
||||||
|
{endButtons}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderName(oobName: string): JSX.Element {
|
||||||
|
let contextMenu: JSX.Element | null = null;
|
||||||
|
if (this.state.contextMenuPosition && this.props.room) {
|
||||||
|
contextMenu = (
|
||||||
|
<RoomContextMenu
|
||||||
|
{...contextMenuBelow(this.state.contextMenuPosition)}
|
||||||
|
room={this.props.room}
|
||||||
|
onFinished={this.onContextMenuCloseClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||||
|
let settingsHint = false;
|
||||||
|
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||||
|
if (members) {
|
||||||
|
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
|
||||||
|
const nameEvent = this.props.room.currentState.getStateEvents("m.room.name", "");
|
||||||
|
if (!nameEvent || !nameEvent.getContent().name) {
|
||||||
|
settingsHint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textClasses = classNames("mx_LegacyRoomHeader_nametext", {
|
||||||
|
mx_LegacyRoomHeader_settingsHint: settingsHint,
|
||||||
|
});
|
||||||
|
const roomName = (
|
||||||
|
<RoomName room={this.props.room}>
|
||||||
|
{(name) => {
|
||||||
|
const roomName = name || oobName;
|
||||||
|
return (
|
||||||
|
<div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
|
||||||
|
{roomName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</RoomName>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.props.enableRoomOptionsMenu && shouldShowComponent(UIComponent.RoomOptionsMenu)) {
|
||||||
|
return (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_LegacyRoomHeader_name"
|
||||||
|
onClick={this.onContextMenuOpenClick}
|
||||||
|
isExpanded={!!this.state.contextMenuPosition}
|
||||||
|
title={_t("Room options")}
|
||||||
|
alignment={Alignment.Bottom}
|
||||||
|
>
|
||||||
|
{roomName}
|
||||||
|
{this.props.room && <div className="mx_LegacyRoomHeader_chevron" />}
|
||||||
|
{contextMenu}
|
||||||
|
</ContextMenuTooltipButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_LegacyRoomHeader_name mx_LegacyRoomHeader_name--textonly">{roomName}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
|
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||||
|
|
||||||
|
let roomAvatar: JSX.Element | null = null;
|
||||||
|
if (this.props.room) {
|
||||||
|
roomAvatar = (
|
||||||
|
<DecoratedRoomAvatar
|
||||||
|
room={this.props.room}
|
||||||
|
avatarSize={24}
|
||||||
|
oobData={this.props.oobData}
|
||||||
|
viewAvatarOnClick={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = this.props.viewingCall ? (
|
||||||
|
<div className="mx_LegacyRoomHeader_icon mx_LegacyRoomHeader_icon_video" />
|
||||||
|
) : this.props.e2eStatus ? (
|
||||||
|
<E2EIcon
|
||||||
|
className="mx_LegacyRoomHeader_icon"
|
||||||
|
status={this.props.e2eStatus}
|
||||||
|
tooltipAlignment={Alignment.Bottom}
|
||||||
|
/>
|
||||||
|
) : // If we're expecting an E2EE status to come in, but it hasn't
|
||||||
|
// yet been loaded, insert a blank div to reserve space
|
||||||
|
this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled() ? (
|
||||||
|
<div className="mx_LegacyRoomHeader_icon" />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
|
||||||
|
|
||||||
|
let oobName = _t("Join Room");
|
||||||
|
if (this.props.oobData && this.props.oobData.name) {
|
||||||
|
oobName = this.props.oobData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = this.renderName(oobName);
|
||||||
|
|
||||||
|
if (this.props.viewingCall && !isVideoRoom) {
|
||||||
|
return (
|
||||||
|
<header className="mx_LegacyRoomHeader light-panel">
|
||||||
|
<div
|
||||||
|
className="mx_LegacyRoomHeader_wrapper"
|
||||||
|
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||||
|
>
|
||||||
|
<div className="mx_LegacyRoomHeader_avatar">{roomAvatar}</div>
|
||||||
|
{icon}
|
||||||
|
{name}
|
||||||
|
{this.props.activeCall instanceof ElementCall && (
|
||||||
|
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
|
||||||
|
)}
|
||||||
|
{/* Empty topic element to fill out space */}
|
||||||
|
<div className="mx_LegacyRoomHeader_topic" />
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchStatus: JSX.Element | null = null;
|
||||||
|
|
||||||
|
// don't display the search count until the search completes and
|
||||||
|
// gives us a valid (possibly zero) searchCount.
|
||||||
|
if (typeof this.props.searchInfo?.count === "number") {
|
||||||
|
searchStatus = (
|
||||||
|
<div className="mx_LegacyRoomHeader_searchStatus">
|
||||||
|
|
||||||
|
{_t("(~%(count)s results)", { count: this.props.searchInfo.count })}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const topicElement = <RoomTopic room={this.props.room} className="mx_LegacyRoomHeader_topic" />;
|
||||||
|
|
||||||
|
const viewLabs = (): void =>
|
||||||
|
defaultDispatcher.dispatch({
|
||||||
|
action: Action.ViewUserSettings,
|
||||||
|
initialTabId: UserTab.Labs,
|
||||||
|
});
|
||||||
|
const betaPill = isVideoRoom ? (
|
||||||
|
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="mx_LegacyRoomHeader light-panel">
|
||||||
|
<div
|
||||||
|
className="mx_LegacyRoomHeader_wrapper"
|
||||||
|
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||||
|
>
|
||||||
|
<div className="mx_LegacyRoomHeader_avatar">{roomAvatar}</div>
|
||||||
|
{icon}
|
||||||
|
{name}
|
||||||
|
{searchStatus}
|
||||||
|
{topicElement}
|
||||||
|
{betaPill}
|
||||||
|
{buttons}
|
||||||
|
</div>
|
||||||
|
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
|
||||||
|
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,805 +14,41 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { FC, useState, useMemo, useCallback } from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
|
||||||
import { throttle } from "lodash";
|
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
|
||||||
|
|
||||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
|
||||||
import { UserTab } from "../dialogs/UserTab";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
|
||||||
import RoomHeaderButtons from "../right_panel/RoomHeaderButtons";
|
|
||||||
import E2EIcon from "./E2EIcon";
|
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
|
||||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
|
||||||
import RoomTopic from "../elements/RoomTopic";
|
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
|
||||||
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
import { IOOBData } from "../../../stores/ThreepidInviteStore";
|
||||||
import { SearchScope } from "./SearchBar";
|
|
||||||
import { aboveLeftOf, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
|
||||||
import RoomContextMenu from "../context_menus/RoomContextMenu";
|
|
||||||
import { contextMenuBelow } from "./RoomTile";
|
|
||||||
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
|
|
||||||
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
|
|
||||||
import { NotificationStateEvents } from "../../../stores/notifications/NotificationState";
|
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
|
||||||
import RoomLiveShareWarning from "../beacon/RoomLiveShareWarning";
|
|
||||||
import { BetaPill } from "../beta/BetaCard";
|
|
||||||
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
|
|
||||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
|
||||||
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
|
|
||||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
|
|
||||||
import { useFeatureEnabled, useSettingValue } from "../../../hooks/useSettings";
|
|
||||||
import SdkConfig from "../../../SdkConfig";
|
|
||||||
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
|
||||||
import { useWidgets } from "../right_panel/RoomSummaryCard";
|
|
||||||
import { WidgetType } from "../../../widgets/WidgetType";
|
|
||||||
import { useCall, useLayout } from "../../../hooks/useCall";
|
|
||||||
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
|
||||||
import { Call, ElementCall, Layout } from "../../../models/Call";
|
|
||||||
import IconizedContextMenu, {
|
|
||||||
IconizedContextMenuOption,
|
|
||||||
IconizedContextMenuOptionList,
|
|
||||||
IconizedContextMenuRadio,
|
|
||||||
} from "../context_menus/IconizedContextMenu";
|
|
||||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
|
||||||
import { GroupCallDuration } from "../voip/CallDuration";
|
|
||||||
import { Alignment } from "../elements/Tooltip";
|
|
||||||
import RoomCallBanner from "../beacon/RoomCallBanner";
|
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
|
||||||
import { UIComponent } from "../../../settings/UIFeature";
|
|
||||||
|
|
||||||
class DisabledWithReason {
|
export default function RoomHeader({ room, oobData }: { room?: Room; oobData?: IOOBData }): JSX.Element {
|
||||||
public constructor(public readonly reason: string) {}
|
let oobName = _t("Join Room");
|
||||||
}
|
if (oobData && oobData.name) {
|
||||||
|
oobName = oobData.name;
|
||||||
interface VoiceCallButtonProps {
|
|
||||||
room: Room;
|
|
||||||
busy: boolean;
|
|
||||||
setBusy: (value: boolean) => void;
|
|
||||||
behavior: DisabledWithReason | "legacy_or_jitsi";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button for starting voice calls, supporting only legacy 1:1 calls and Jitsi
|
|
||||||
* widgets.
|
|
||||||
*/
|
|
||||||
const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavior }) => {
|
|
||||||
const { onClick, tooltip, disabled } = useMemo(() => {
|
|
||||||
if (behavior instanceof DisabledWithReason) {
|
|
||||||
return {
|
|
||||||
onClick: () => {},
|
|
||||||
tooltip: behavior.reason,
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// behavior === "legacy_or_jitsi"
|
|
||||||
return {
|
|
||||||
onClick: async (ev: ButtonEvent): Promise<void> => {
|
|
||||||
ev.preventDefault();
|
|
||||||
setBusy(true);
|
|
||||||
await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Voice);
|
|
||||||
setBusy(false);
|
|
||||||
},
|
|
||||||
disabled: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [behavior, room, setBusy]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_voiceCallButton"
|
|
||||||
onClick={onClick}
|
|
||||||
title={_t("Voice call")}
|
|
||||||
tooltip={tooltip ?? _t("Voice call")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
disabled={disabled || busy}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface VideoCallButtonProps {
|
|
||||||
room: Room;
|
|
||||||
busy: boolean;
|
|
||||||
setBusy: (value: boolean) => void;
|
|
||||||
behavior: DisabledWithReason | "legacy_or_jitsi" | "element" | "jitsi_or_element";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Button for starting video calls, supporting both legacy 1:1 calls, Jitsi
|
|
||||||
* widgets, and native group calls. If multiple calling options are available,
|
|
||||||
* this shows a menu to pick between them.
|
|
||||||
*/
|
|
||||||
const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavior }) => {
|
|
||||||
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
|
||||||
|
|
||||||
const startLegacyCall = useCallback(async (): Promise<void> => {
|
|
||||||
setBusy(true);
|
|
||||||
await LegacyCallHandler.instance.placeCall(room.roomId, CallType.Video);
|
|
||||||
setBusy(false);
|
|
||||||
}, [setBusy, room]);
|
|
||||||
|
|
||||||
const startElementCall = useCallback(() => {
|
|
||||||
setBusy(true);
|
|
||||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
view_call: true,
|
|
||||||
metricsTrigger: undefined,
|
|
||||||
});
|
|
||||||
setBusy(false);
|
|
||||||
}, [setBusy, room]);
|
|
||||||
|
|
||||||
const { onClick, tooltip, disabled } = useMemo(() => {
|
|
||||||
if (behavior instanceof DisabledWithReason) {
|
|
||||||
return {
|
|
||||||
onClick: () => {},
|
|
||||||
tooltip: behavior.reason,
|
|
||||||
disabled: true,
|
|
||||||
};
|
|
||||||
} else if (behavior === "legacy_or_jitsi") {
|
|
||||||
return {
|
|
||||||
onClick: async (ev: ButtonEvent): Promise<void> => {
|
|
||||||
ev.preventDefault();
|
|
||||||
await startLegacyCall();
|
|
||||||
},
|
|
||||||
disabled: false,
|
|
||||||
};
|
|
||||||
} else if (behavior === "element") {
|
|
||||||
return {
|
|
||||||
onClick: async (ev: ButtonEvent): Promise<void> => {
|
|
||||||
ev.preventDefault();
|
|
||||||
startElementCall();
|
|
||||||
},
|
|
||||||
disabled: false,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// behavior === "jitsi_or_element"
|
|
||||||
return {
|
|
||||||
onClick: async (ev: ButtonEvent): Promise<void> => {
|
|
||||||
ev.preventDefault();
|
|
||||||
openMenu();
|
|
||||||
},
|
|
||||||
disabled: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [behavior, startLegacyCall, startElementCall, openMenu]);
|
|
||||||
|
|
||||||
const onJitsiClick = useCallback(
|
|
||||||
async (ev: ButtonEvent): Promise<void> => {
|
|
||||||
ev.preventDefault();
|
|
||||||
closeMenu();
|
|
||||||
await startLegacyCall();
|
|
||||||
},
|
|
||||||
[closeMenu, startLegacyCall],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onElementClick = useCallback(
|
|
||||||
(ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
closeMenu();
|
|
||||||
startElementCall();
|
|
||||||
},
|
|
||||||
[closeMenu, startElementCall],
|
|
||||||
);
|
|
||||||
|
|
||||||
let menu: JSX.Element | null = null;
|
|
||||||
if (menuOpen) {
|
|
||||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
|
||||||
const brand = SdkConfig.get("element_call").brand;
|
|
||||||
menu = (
|
|
||||||
<IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
|
||||||
<IconizedContextMenuOptionList>
|
|
||||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
label={_t("Video call (%(brand)s)", { brand })}
|
|
||||||
onClick={onElementClick}
|
|
||||||
/>
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<header className="mx_LegacyRoomHeader light-panel">
|
||||||
<AccessibleTooltipButton
|
<div className="mx_LegacyRoomHeader_wrapper">
|
||||||
inputRef={buttonRef}
|
{room && (
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_videoCallButton"
|
<RoomName room={room}>
|
||||||
onClick={onClick}
|
{(name) => {
|
||||||
title={_t("Video call")}
|
const roomName = name || oobName;
|
||||||
tooltip={tooltip ?? _t("Video call")}
|
return (
|
||||||
alignment={Alignment.Bottom}
|
<div
|
||||||
disabled={disabled || busy}
|
className="mx_LegacyRoomHeader_name"
|
||||||
/>
|
dir="auto"
|
||||||
{menu}
|
title={roomName}
|
||||||
</>
|
role="heading"
|
||||||
|
aria-level={1}
|
||||||
|
>
|
||||||
|
{roomName}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</RoomName>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
interface CallButtonsProps {
|
|
||||||
room: Room;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The header buttons for placing calls have become stupidly complex, so here
|
|
||||||
// they are as a separate component
|
|
||||||
const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|
||||||
const [busy, setBusy] = useState(false);
|
|
||||||
const showButtons = useSettingValue<boolean>("showCallButtonsInComposer");
|
|
||||||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
|
||||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
|
||||||
const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, room]);
|
|
||||||
const useElementCallExclusively = useMemo(() => {
|
|
||||||
return SdkConfig.get("element_call").use_exclusively;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const hasLegacyCall = useEventEmitterState(
|
|
||||||
LegacyCallHandler.instance,
|
|
||||||
LegacyCallHandlerEvent.CallsChanged,
|
|
||||||
useCallback(() => LegacyCallHandler.instance.getCallForRoom(room.roomId) !== null, [room]),
|
|
||||||
);
|
|
||||||
|
|
||||||
const widgets = useWidgets(room);
|
|
||||||
const hasJitsiWidget = useMemo(() => widgets.some((widget) => WidgetType.JITSI.matches(widget.type)), [widgets]);
|
|
||||||
|
|
||||||
const hasGroupCall = useCall(room.roomId) !== null;
|
|
||||||
|
|
||||||
const [functionalMembers, mayEditWidgets, mayCreateElementCalls] = useTypedEventEmitterState(
|
|
||||||
room,
|
|
||||||
RoomStateEvent.Update,
|
|
||||||
useCallback(
|
|
||||||
() => [
|
|
||||||
getJoinedNonFunctionalMembers(room),
|
|
||||||
room.currentState.mayClientSendStateEvent("im.vector.modular.widgets", room.client),
|
|
||||||
room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, room.client),
|
|
||||||
],
|
|
||||||
[room],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const makeVoiceCallButton = (behavior: VoiceCallButtonProps["behavior"]): JSX.Element => (
|
|
||||||
<VoiceCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
|
|
||||||
);
|
|
||||||
const makeVideoCallButton = (behavior: VideoCallButtonProps["behavior"]): JSX.Element => (
|
|
||||||
<VideoCallButton room={room} busy={busy} setBusy={setBusy} behavior={behavior} />
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isVideoRoom || !showButtons) {
|
|
||||||
return null;
|
|
||||||
} else if (groupCallsEnabled) {
|
|
||||||
if (useElementCallExclusively) {
|
|
||||||
if (hasGroupCall) {
|
|
||||||
return makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")));
|
|
||||||
} else if (mayCreateElementCalls) {
|
|
||||||
return makeVideoCallButton("element");
|
|
||||||
} else {
|
|
||||||
return makeVideoCallButton(
|
|
||||||
new DisabledWithReason(_t("You do not have permission to start video calls")),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
|
||||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (functionalMembers.length <= 1) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
|
||||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (functionalMembers.length === 2) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
|
||||||
{makeVideoCallButton("legacy_or_jitsi")}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (mayEditWidgets) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
|
||||||
{makeVideoCallButton(mayCreateElementCalls ? "jitsi_or_element" : "legacy_or_jitsi")}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const videoCallBehavior = mayCreateElementCalls
|
|
||||||
? "element"
|
|
||||||
: new DisabledWithReason(_t("You do not have permission to start video calls"));
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
|
||||||
{makeVideoCallButton(videoCallBehavior)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (hasLegacyCall || hasJitsiWidget) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
|
||||||
{makeVideoCallButton(new DisabledWithReason(_t("Ongoing call")))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (functionalMembers.length <= 1) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
|
||||||
{makeVideoCallButton(new DisabledWithReason(_t("There's no one here to call")))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else if (functionalMembers.length === 2 || mayEditWidgets) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton("legacy_or_jitsi")}
|
|
||||||
{makeVideoCallButton("legacy_or_jitsi")}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{makeVoiceCallButton(new DisabledWithReason(_t("You do not have permission to start voice calls")))}
|
|
||||||
{makeVideoCallButton(new DisabledWithReason(_t("You do not have permission to start video calls")))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CallLayoutSelectorProps {
|
|
||||||
call: ElementCall;
|
|
||||||
}
|
|
||||||
|
|
||||||
const CallLayoutSelector: FC<CallLayoutSelectorProps> = ({ call }) => {
|
|
||||||
const layout = useLayout(call);
|
|
||||||
const [menuOpen, buttonRef, openMenu, closeMenu] = useContextMenu();
|
|
||||||
|
|
||||||
const onClick = useCallback(
|
|
||||||
(ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
openMenu();
|
|
||||||
},
|
|
||||||
[openMenu],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onFreedomClick = useCallback(
|
|
||||||
(ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
closeMenu();
|
|
||||||
call.setLayout(Layout.Tile);
|
|
||||||
},
|
|
||||||
[closeMenu, call],
|
|
||||||
);
|
|
||||||
|
|
||||||
const onSpotlightClick = useCallback(
|
|
||||||
(ev: ButtonEvent) => {
|
|
||||||
ev.preventDefault();
|
|
||||||
closeMenu();
|
|
||||||
call.setLayout(Layout.Spotlight);
|
|
||||||
},
|
|
||||||
[closeMenu, call],
|
|
||||||
);
|
|
||||||
|
|
||||||
let menu: JSX.Element | null = null;
|
|
||||||
if (menuOpen) {
|
|
||||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
|
||||||
menu = (
|
|
||||||
<IconizedContextMenu
|
|
||||||
className="mx_RoomHeader_layoutMenu"
|
|
||||||
{...aboveLeftOf(buttonRect)}
|
|
||||||
onFinished={closeMenu}
|
|
||||||
>
|
|
||||||
<IconizedContextMenuOptionList>
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
iconClassName="mx_RoomHeader_freedomIcon"
|
|
||||||
label={_t("Freedom")}
|
|
||||||
active={layout === Layout.Tile}
|
|
||||||
onClick={onFreedomClick}
|
|
||||||
/>
|
|
||||||
<IconizedContextMenuRadio
|
|
||||||
iconClassName="mx_RoomHeader_spotlightIcon"
|
|
||||||
label={_t("Spotlight")}
|
|
||||||
active={layout === Layout.Spotlight}
|
|
||||||
onClick={onSpotlightClick}
|
|
||||||
/>
|
|
||||||
</IconizedContextMenuOptionList>
|
|
||||||
</IconizedContextMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
inputRef={buttonRef}
|
|
||||||
className={classNames("mx_RoomHeader_button", {
|
|
||||||
"mx_RoomHeader_layoutButton--freedom": layout === Layout.Tile,
|
|
||||||
"mx_RoomHeader_layoutButton--spotlight": layout === Layout.Spotlight,
|
|
||||||
})}
|
|
||||||
onClick={onClick}
|
|
||||||
title={_t("Change layout")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
key="layout"
|
|
||||||
/>
|
|
||||||
{menu}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface ISearchInfo {
|
|
||||||
searchId: number;
|
|
||||||
roomId?: string;
|
|
||||||
term: string;
|
|
||||||
scope: SearchScope;
|
|
||||||
promise: Promise<ISearchResults>;
|
|
||||||
abortController?: AbortController;
|
|
||||||
|
|
||||||
inProgress?: boolean;
|
|
||||||
count?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IProps {
|
|
||||||
room: Room;
|
|
||||||
oobData?: IOOBData;
|
|
||||||
inRoom: boolean;
|
|
||||||
onSearchClick: (() => void) | null;
|
|
||||||
onInviteClick: (() => void) | null;
|
|
||||||
onForgetClick: (() => void) | null;
|
|
||||||
onAppsClick: (() => void) | null;
|
|
||||||
e2eStatus: E2EStatus;
|
|
||||||
appsShown: boolean;
|
|
||||||
searchInfo?: ISearchInfo;
|
|
||||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
|
||||||
showButtons?: boolean;
|
|
||||||
enableRoomOptionsMenu?: boolean;
|
|
||||||
viewingCall: boolean;
|
|
||||||
activeCall: Call | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
|
||||||
contextMenuPosition?: DOMRect;
|
|
||||||
rightPanelOpen: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class RoomHeader extends React.Component<IProps, IState> {
|
|
||||||
public static defaultProps: Partial<IProps> = {
|
|
||||||
inRoom: false,
|
|
||||||
excludedRightPanelPhaseButtons: [],
|
|
||||||
showButtons: true,
|
|
||||||
enableRoomOptionsMenu: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
public static contextType = RoomContext;
|
|
||||||
public context!: React.ContextType<typeof RoomContext>;
|
|
||||||
private readonly client = this.props.room.client;
|
|
||||||
|
|
||||||
public constructor(props: IProps, context: IState) {
|
|
||||||
super(props, context);
|
|
||||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(props.room);
|
|
||||||
notiStore.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
|
||||||
this.state = {
|
|
||||||
rightPanelOpen: RightPanelStore.instance.isOpen,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
|
||||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
|
||||||
this.client.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
|
||||||
const notiStore = RoomNotificationStateStore.instance.getRoomState(this.props.room);
|
|
||||||
notiStore.removeListener(NotificationStateEvents.Update, this.onNotificationUpdate);
|
|
||||||
RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onRightPanelStoreUpdate = (): void => {
|
|
||||||
this.setState({ rightPanelOpen: RightPanelStore.instance.isOpen });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRoomStateEvents = (event: MatrixEvent): void => {
|
|
||||||
if (!this.props.room || event.getRoomId() !== this.props.room.roomId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// redisplay the room name, topic, etc.
|
|
||||||
this.rateLimitedUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onNotificationUpdate = (): void => {
|
|
||||||
this.forceUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
private rateLimitedUpdate = throttle(
|
|
||||||
() => {
|
|
||||||
this.forceUpdate();
|
|
||||||
},
|
|
||||||
500,
|
|
||||||
{ leading: true, trailing: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
private onContextMenuOpenClick = (ev: ButtonEvent): void => {
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
const target = ev.target as HTMLButtonElement;
|
|
||||||
this.setState({ contextMenuPosition: target.getBoundingClientRect() });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onContextMenuCloseClick = (): void => {
|
|
||||||
this.setState({ contextMenuPosition: undefined });
|
|
||||||
};
|
|
||||||
|
|
||||||
private onHideCallClick = (ev: ButtonEvent): void => {
|
|
||||||
ev.preventDefault();
|
|
||||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
view_call: false,
|
|
||||||
metricsTrigger: undefined,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private renderButtons(isVideoRoom: boolean): React.ReactNode {
|
|
||||||
const startButtons: JSX.Element[] = [];
|
|
||||||
|
|
||||||
if (!this.props.viewingCall && this.props.inRoom && !this.context.tombstone) {
|
|
||||||
startButtons.push(<CallButtons key="calls" room={this.props.room} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
|
|
||||||
startButtons.push(<CallLayoutSelector key="layout" call={this.props.activeCall} />);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.viewingCall && this.props.onForgetClick) {
|
|
||||||
startButtons.push(
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_forgetButton"
|
|
||||||
onClick={this.props.onForgetClick}
|
|
||||||
title={_t("Forget room")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
key="forget"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.viewingCall && this.props.onAppsClick) {
|
|
||||||
startButtons.push(
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className={classNames("mx_RoomHeader_button mx_RoomHeader_appsButton", {
|
|
||||||
mx_RoomHeader_appsButton_highlight: this.props.appsShown,
|
|
||||||
})}
|
|
||||||
onClick={this.props.onAppsClick}
|
|
||||||
title={this.props.appsShown ? _t("Hide Widgets") : _t("Show Widgets")}
|
|
||||||
aria-checked={this.props.appsShown}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
key="apps"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.props.viewingCall && this.props.onSearchClick && this.props.inRoom) {
|
|
||||||
startButtons.push(
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_searchButton"
|
|
||||||
onClick={this.props.onSearchClick}
|
|
||||||
title={_t("Search")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
key="search"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.onInviteClick && (!this.props.viewingCall || isVideoRoom) && this.props.inRoom) {
|
|
||||||
startButtons.push(
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_inviteButton"
|
|
||||||
onClick={this.props.onInviteClick}
|
|
||||||
title={_t("Invite")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
key="invite"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const endButtons: JSX.Element[] = [];
|
|
||||||
|
|
||||||
if (this.props.viewingCall && !isVideoRoom) {
|
|
||||||
if (this.props.activeCall === null) {
|
|
||||||
endButtons.push(
|
|
||||||
<AccessibleButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_closeButton"
|
|
||||||
onClick={this.onHideCallClick}
|
|
||||||
title={_t("Close call")}
|
|
||||||
key="close"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
endButtons.push(
|
|
||||||
<AccessibleTooltipButton
|
|
||||||
className="mx_RoomHeader_button mx_RoomHeader_minimiseButton"
|
|
||||||
onClick={this.onHideCallClick}
|
|
||||||
title={_t("View chat timeline")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
key="minimise"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{startButtons}
|
|
||||||
<RoomHeaderButtons
|
|
||||||
room={this.props.room}
|
|
||||||
excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons}
|
|
||||||
/>
|
|
||||||
{endButtons}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderName(oobName: string): JSX.Element {
|
|
||||||
let contextMenu: JSX.Element | null = null;
|
|
||||||
if (this.state.contextMenuPosition && this.props.room) {
|
|
||||||
contextMenu = (
|
|
||||||
<RoomContextMenu
|
|
||||||
{...contextMenuBelow(this.state.contextMenuPosition)}
|
|
||||||
room={this.props.room}
|
|
||||||
onFinished={this.onContextMenuCloseClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
|
||||||
let settingsHint = false;
|
|
||||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
|
||||||
if (members) {
|
|
||||||
if (members.length === 1 && members[0].userId === this.client.credentials.userId) {
|
|
||||||
const nameEvent = this.props.room.currentState.getStateEvents("m.room.name", "");
|
|
||||||
if (!nameEvent || !nameEvent.getContent().name) {
|
|
||||||
settingsHint = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const textClasses = classNames("mx_RoomHeader_nametext", { mx_RoomHeader_settingsHint: settingsHint });
|
|
||||||
const roomName = (
|
|
||||||
<RoomName room={this.props.room}>
|
|
||||||
{(name) => {
|
|
||||||
const roomName = name || oobName;
|
|
||||||
return (
|
|
||||||
<div dir="auto" className={textClasses} title={roomName} role="heading" aria-level={1}>
|
|
||||||
{roomName}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</RoomName>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.props.enableRoomOptionsMenu && shouldShowComponent(UIComponent.RoomOptionsMenu)) {
|
|
||||||
return (
|
|
||||||
<ContextMenuTooltipButton
|
|
||||||
className="mx_RoomHeader_name"
|
|
||||||
onClick={this.onContextMenuOpenClick}
|
|
||||||
isExpanded={!!this.state.contextMenuPosition}
|
|
||||||
title={_t("Room options")}
|
|
||||||
alignment={Alignment.Bottom}
|
|
||||||
>
|
|
||||||
{roomName}
|
|
||||||
{this.props.room && <div className="mx_RoomHeader_chevron" />}
|
|
||||||
{contextMenu}
|
|
||||||
</ContextMenuTooltipButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">{roomName}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
|
||||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
|
||||||
|
|
||||||
let roomAvatar: JSX.Element | null = null;
|
|
||||||
if (this.props.room) {
|
|
||||||
roomAvatar = (
|
|
||||||
<DecoratedRoomAvatar
|
|
||||||
room={this.props.room}
|
|
||||||
avatarSize={24}
|
|
||||||
oobData={this.props.oobData}
|
|
||||||
viewAvatarOnClick={true}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const icon = this.props.viewingCall ? (
|
|
||||||
<div className="mx_RoomHeader_icon mx_RoomHeader_icon_video" />
|
|
||||||
) : this.props.e2eStatus ? (
|
|
||||||
<E2EIcon className="mx_RoomHeader_icon" status={this.props.e2eStatus} tooltipAlignment={Alignment.Bottom} />
|
|
||||||
) : // If we're expecting an E2EE status to come in, but it hasn't
|
|
||||||
// yet been loaded, insert a blank div to reserve space
|
|
||||||
this.client.isRoomEncrypted(this.props.room.roomId) && this.client.isCryptoEnabled() ? (
|
|
||||||
<div className="mx_RoomHeader_icon" />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
const buttons = this.props.showButtons ? this.renderButtons(isVideoRoom) : null;
|
|
||||||
|
|
||||||
let oobName = _t("Join Room");
|
|
||||||
if (this.props.oobData && this.props.oobData.name) {
|
|
||||||
oobName = this.props.oobData.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = this.renderName(oobName);
|
|
||||||
|
|
||||||
if (this.props.viewingCall && !isVideoRoom) {
|
|
||||||
return (
|
|
||||||
<header className="mx_RoomHeader light-panel">
|
|
||||||
<div
|
|
||||||
className="mx_RoomHeader_wrapper"
|
|
||||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
|
||||||
>
|
|
||||||
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
|
|
||||||
{icon}
|
|
||||||
{name}
|
|
||||||
{this.props.activeCall instanceof ElementCall && (
|
|
||||||
<GroupCallDuration groupCall={this.props.activeCall.groupCall} />
|
|
||||||
)}
|
|
||||||
{/* Empty topic element to fill out space */}
|
|
||||||
<div className="mx_RoomHeader_topic" />
|
|
||||||
{buttons}
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let searchStatus: JSX.Element | null = null;
|
|
||||||
|
|
||||||
// don't display the search count until the search completes and
|
|
||||||
// gives us a valid (possibly zero) searchCount.
|
|
||||||
if (typeof this.props.searchInfo?.count === "number") {
|
|
||||||
searchStatus = (
|
|
||||||
<div className="mx_RoomHeader_searchStatus">
|
|
||||||
|
|
||||||
{_t("(~%(count)s results)", { count: this.props.searchInfo.count })}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const topicElement = <RoomTopic room={this.props.room} className="mx_RoomHeader_topic" />;
|
|
||||||
|
|
||||||
const viewLabs = (): void =>
|
|
||||||
defaultDispatcher.dispatch({
|
|
||||||
action: Action.ViewUserSettings,
|
|
||||||
initialTabId: UserTab.Labs,
|
|
||||||
});
|
|
||||||
const betaPill = isVideoRoom ? (
|
|
||||||
<BetaPill onClick={viewLabs} tooltipTitle={_t("Video rooms are a beta feature")} />
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header className="mx_RoomHeader light-panel">
|
|
||||||
<div
|
|
||||||
className="mx_RoomHeader_wrapper"
|
|
||||||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
|
||||||
>
|
|
||||||
<div className="mx_RoomHeader_avatar">{roomAvatar}</div>
|
|
||||||
{icon}
|
|
||||||
{name}
|
|
||||||
{searchStatus}
|
|
||||||
{topicElement}
|
|
||||||
{betaPill}
|
|
||||||
{buttons}
|
|
||||||
</div>
|
|
||||||
{!isVideoRoom && <RoomCallBanner roomId={this.props.room.roomId} />}
|
|
||||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1009,6 +1009,7 @@
|
||||||
"Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)",
|
"Hide notification dot (only display counters badges)": "Hide notification dot (only display counters badges)",
|
||||||
"Enable intentional mentions": "Enable intentional mentions",
|
"Enable intentional mentions": "Enable intentional mentions",
|
||||||
"Enable ask to join": "Enable ask to join",
|
"Enable ask to join": "Enable ask to join",
|
||||||
|
"Under active development, new room header & details interface": "Under active development, new room header & details interface",
|
||||||
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
|
"Use a more compact 'Modern' layout": "Use a more compact 'Modern' layout",
|
||||||
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
"Show a placeholder for removed messages": "Show a placeholder for removed messages",
|
||||||
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
|
"Show join/leave messages (invites/removes/bans unaffected)": "Show join/leave messages (invites/removes/bans unaffected)",
|
||||||
|
@ -1941,6 +1942,26 @@
|
||||||
"Encrypted messages before this point are unavailable.": "Encrypted messages before this point are unavailable.",
|
"Encrypted messages before this point are unavailable.": "Encrypted messages before this point are unavailable.",
|
||||||
"You can't see earlier messages": "You can't see earlier messages",
|
"You can't see earlier messages": "You can't see earlier messages",
|
||||||
"Scroll to most recent messages": "Scroll to most recent messages",
|
"Scroll to most recent messages": "Scroll to most recent messages",
|
||||||
|
"Video call (Jitsi)": "Video call (Jitsi)",
|
||||||
|
"Video call (%(brand)s)": "Video call (%(brand)s)",
|
||||||
|
"Ongoing call": "Ongoing call",
|
||||||
|
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
||||||
|
"There's no one here to call": "There's no one here to call",
|
||||||
|
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
||||||
|
"Freedom": "Freedom",
|
||||||
|
"Spotlight": "Spotlight",
|
||||||
|
"Change layout": "Change layout",
|
||||||
|
"Forget room": "Forget room",
|
||||||
|
"Hide Widgets": "Hide Widgets",
|
||||||
|
"Show Widgets": "Show Widgets",
|
||||||
|
"Search": "Search",
|
||||||
|
"Close call": "Close call",
|
||||||
|
"View chat timeline": "View chat timeline",
|
||||||
|
"Room options": "Room options",
|
||||||
|
"Join Room": "Join Room",
|
||||||
|
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||||
|
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||||
|
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
||||||
"Show %(count)s other previews|other": "Show %(count)s other previews",
|
"Show %(count)s other previews|other": "Show %(count)s other previews",
|
||||||
"Show %(count)s other previews|one": "Show %(count)s other preview",
|
"Show %(count)s other previews|one": "Show %(count)s other preview",
|
||||||
"Close preview": "Close preview",
|
"Close preview": "Close preview",
|
||||||
|
@ -2018,26 +2039,6 @@
|
||||||
"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",
|
||||||
"Video call (Jitsi)": "Video call (Jitsi)",
|
|
||||||
"Video call (%(brand)s)": "Video call (%(brand)s)",
|
|
||||||
"Ongoing call": "Ongoing call",
|
|
||||||
"You do not have permission to start video calls": "You do not have permission to start video calls",
|
|
||||||
"There's no one here to call": "There's no one here to call",
|
|
||||||
"You do not have permission to start voice calls": "You do not have permission to start voice calls",
|
|
||||||
"Freedom": "Freedom",
|
|
||||||
"Spotlight": "Spotlight",
|
|
||||||
"Change layout": "Change layout",
|
|
||||||
"Forget room": "Forget room",
|
|
||||||
"Hide Widgets": "Hide Widgets",
|
|
||||||
"Show Widgets": "Show Widgets",
|
|
||||||
"Search": "Search",
|
|
||||||
"Close call": "Close call",
|
|
||||||
"View chat timeline": "View chat timeline",
|
|
||||||
"Room options": "Room options",
|
|
||||||
"Join Room": "Join Room",
|
|
||||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
|
||||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
|
||||||
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
|
||||||
"Video room": "Video room",
|
"Video room": "Video room",
|
||||||
"Public space": "Public space",
|
"Public space": "Public space",
|
||||||
"Public room": "Public room",
|
"Public room": "Public room",
|
||||||
|
@ -2242,11 +2243,11 @@
|
||||||
"Yours, or the other users' session": "Yours, or the other users' session",
|
"Yours, or the other users' session": "Yours, or the other users' session",
|
||||||
"Error starting verification": "Error starting verification",
|
"Error starting verification": "Error starting verification",
|
||||||
"We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.",
|
"We were unable to start a chat with the other user.": "We were unable to start a chat with the other user.",
|
||||||
"Nothing pinned, yet": "Nothing pinned, yet",
|
|
||||||
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
|
||||||
"Pinned messages": "Pinned messages",
|
"Pinned messages": "Pinned messages",
|
||||||
"Chat": "Chat",
|
"Chat": "Chat",
|
||||||
"Room info": "Room info",
|
"Room info": "Room info",
|
||||||
|
"Nothing pinned, yet": "Nothing pinned, yet",
|
||||||
|
"If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.": "If you have permissions, open the menu on any message and select <b>Pin</b> to stick them here.",
|
||||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||||
"Maximise": "Maximise",
|
"Maximise": "Maximise",
|
||||||
"Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
|
"Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
|
||||||
|
|
|
@ -566,6 +566,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
labsGroup: LabGroup.Rooms,
|
labsGroup: LabGroup.Rooms,
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
},
|
},
|
||||||
|
"feature_new_room_decoration_ui": {
|
||||||
|
isFeature: true,
|
||||||
|
labsGroup: LabGroup.Rooms,
|
||||||
|
displayName: _td("Under active development, new room header & details interface"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
controller: new ReloadOnChangeController(),
|
||||||
|
},
|
||||||
"useCompactLayout": {
|
"useCompactLayout": {
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
|
||||||
displayName: _td("Use a more compact 'Modern' layout"),
|
displayName: _td("Use a more compact 'Modern' layout"),
|
||||||
|
|
|
@ -181,23 +181,23 @@ export default class HTMLExporter extends Exporter {
|
||||||
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
|
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
|
||||||
<div class="mx_MatrixChat">
|
<div class="mx_MatrixChat">
|
||||||
<main class="mx_RoomView">
|
<main class="mx_RoomView">
|
||||||
<div class="mx_RoomHeader light-panel">
|
<div class="mx_LegacyRoomHeader light-panel">
|
||||||
<div class="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
<div class="mx_LegacyRoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||||
<div class="mx_RoomHeader_avatar">
|
<div class="mx_LegacyRoomHeader_avatar">
|
||||||
<div class="mx_DecoratedRoomAvatar">
|
<div class="mx_DecoratedRoomAvatar">
|
||||||
${roomAvatar}
|
${roomAvatar}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx_RoomHeader_name">
|
<div class="mx_LegacyRoomHeader_name">
|
||||||
<div
|
<div
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mx_RoomHeader_nametext"
|
class="mx_LegacyRoomHeader_nametext"
|
||||||
title="${safeRoomName}"
|
title="${safeRoomName}"
|
||||||
>
|
>
|
||||||
${safeRoomName}
|
${safeRoomName}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx_RoomHeader_topic" dir="auto"> ${safeTopic} </div>
|
<div class="mx_LegacyRoomHeader_topic" dir="auto"> ${safeTopic} </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${previousMessagesLink}
|
${previousMessagesLink}
|
||||||
|
|
|
@ -6,13 +6,13 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||||
class="mx_RoomView mx_RoomView--local"
|
class="mx_RoomView mx_RoomView--local"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="mx_RoomHeader light-panel"
|
class="mx_LegacyRoomHeader light-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_wrapper"
|
class="mx_LegacyRoomHeader_wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_avatar"
|
class="mx_LegacyRoomHeader_avatar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_DecoratedRoomAvatar"
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
@ -41,11 +41,11 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
class="mx_LegacyRoomHeader_name mx_LegacyRoomHeader_name--textonly"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
class="mx_RoomHeader_nametext"
|
class="mx_LegacyRoomHeader_nametext"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
role="heading"
|
role="heading"
|
||||||
title="@user:example.com"
|
title="@user:example.com"
|
||||||
|
@ -55,7 +55,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-describedby="mx_TooltipTarget_abdefghi"
|
aria-describedby="mx_TooltipTarget_abdefghi"
|
||||||
class="mx_RoomHeader_topic mx_RoomTopic"
|
class="mx_LegacyRoomHeader_topic mx_RoomTopic"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
@ -99,13 +99,13 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||||
class="mx_RoomView mx_RoomView--local"
|
class="mx_RoomView mx_RoomView--local"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="mx_RoomHeader light-panel"
|
class="mx_LegacyRoomHeader light-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_wrapper"
|
class="mx_LegacyRoomHeader_wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_avatar"
|
class="mx_LegacyRoomHeader_avatar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_DecoratedRoomAvatar"
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
@ -134,11 +134,11 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
class="mx_LegacyRoomHeader_name mx_LegacyRoomHeader_name--textonly"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
class="mx_RoomHeader_nametext"
|
class="mx_LegacyRoomHeader_nametext"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
role="heading"
|
role="heading"
|
||||||
title="@user:example.com"
|
title="@user:example.com"
|
||||||
|
@ -148,7 +148,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-describedby="mx_TooltipTarget_abdefghi"
|
aria-describedby="mx_TooltipTarget_abdefghi"
|
||||||
class="mx_RoomHeader_topic mx_RoomTopic"
|
class="mx_LegacyRoomHeader_topic mx_RoomTopic"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
@ -289,13 +289,13 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||||
class="mx_RoomView mx_RoomView--local"
|
class="mx_RoomView mx_RoomView--local"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="mx_RoomHeader light-panel"
|
class="mx_LegacyRoomHeader light-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_wrapper"
|
class="mx_LegacyRoomHeader_wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_avatar"
|
class="mx_LegacyRoomHeader_avatar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_DecoratedRoomAvatar"
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
@ -324,11 +324,11 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
class="mx_LegacyRoomHeader_name mx_LegacyRoomHeader_name--textonly"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
class="mx_RoomHeader_nametext"
|
class="mx_LegacyRoomHeader_nametext"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
role="heading"
|
role="heading"
|
||||||
title="@user:example.com"
|
title="@user:example.com"
|
||||||
|
@ -338,7 +338,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-describedby="mx_TooltipTarget_abdefghi"
|
aria-describedby="mx_TooltipTarget_abdefghi"
|
||||||
class="mx_RoomHeader_topic mx_RoomTopic"
|
class="mx_LegacyRoomHeader_topic mx_RoomTopic"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
@ -554,13 +554,13 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||||
class="mx_RoomView mx_RoomView--local"
|
class="mx_RoomView mx_RoomView--local"
|
||||||
>
|
>
|
||||||
<header
|
<header
|
||||||
class="mx_RoomHeader light-panel"
|
class="mx_LegacyRoomHeader light-panel"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_wrapper"
|
class="mx_LegacyRoomHeader_wrapper"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_avatar"
|
class="mx_LegacyRoomHeader_avatar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_DecoratedRoomAvatar"
|
class="mx_DecoratedRoomAvatar"
|
||||||
|
@ -590,14 +590,14 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-label="This room is end-to-end encrypted"
|
aria-label="This room is end-to-end encrypted"
|
||||||
class="mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon"
|
class="mx_E2EIcon mx_E2EIcon_normal mx_LegacyRoomHeader_icon"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomHeader_name mx_RoomHeader_name--textonly"
|
class="mx_LegacyRoomHeader_name mx_LegacyRoomHeader_name--textonly"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-level="1"
|
aria-level="1"
|
||||||
class="mx_RoomHeader_nametext"
|
class="mx_LegacyRoomHeader_nametext"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
role="heading"
|
role="heading"
|
||||||
title="@user:example.com"
|
title="@user:example.com"
|
||||||
|
@ -607,7 +607,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
aria-describedby="mx_TooltipTarget_abdefghi"
|
aria-describedby="mx_TooltipTarget_abdefghi"
|
||||||
class="mx_RoomHeader_topic mx_RoomTopic"
|
class="mx_LegacyRoomHeader_topic mx_RoomTopic"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
>
|
>
|
||||||
|
|
|
@ -21,12 +21,12 @@ import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons";
|
import LegacyRoomHeaderButtons from "../../../../src/components/views/right_panel/LegacyRoomHeaderButtons";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import { mkEvent, stubClient } from "../../../test-utils";
|
import { mkEvent, stubClient } from "../../../test-utils";
|
||||||
import { mkThread } from "../../../test-utils/threads";
|
import { mkThread } from "../../../test-utils/threads";
|
||||||
|
|
||||||
describe("RoomHeaderButtons-test.tsx", function () {
|
describe("LegacyRoomHeaderButtons-test.tsx", function () {
|
||||||
const ROOM_ID = "!roomId:example.org";
|
const ROOM_ID = "!roomId:example.org";
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let client: MatrixClient;
|
let client: MatrixClient;
|
||||||
|
@ -43,7 +43,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
function getComponent(room?: Room) {
|
function getComponent(room?: Room) {
|
||||||
return render(<RoomHeaderButtons room={room} excludedRightPanelPhaseButtons={[]} />);
|
return render(<LegacyRoomHeaderButtons room={room} excludedRightPanelPhaseButtons={[]} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThreadButton(container: HTMLElement) {
|
function getThreadButton(container: HTMLElement) {
|
||||||
|
@ -75,10 +75,10 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
|
|
||||||
it("thread notification does change the thread button", () => {
|
it("thread notification does change the thread button", () => {
|
||||||
const { container } = getComponent(room);
|
const { container } = getComponent(room);
|
||||||
expect(getThreadButton(container)!.className.includes("mx_RoomHeader_button--unread")).toBeFalsy();
|
expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeFalsy();
|
||||||
|
|
||||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
||||||
expect(getThreadButton(container)!.className.includes("mx_RoomHeader_button--unread")).toBeTruthy();
|
expect(getThreadButton(container)!.className.includes("mx_LegacyRoomHeader_button--unread")).toBeTruthy();
|
||||||
expect(isIndicatorOfType(container, "gray")).toBe(true);
|
expect(isIndicatorOfType(container, "gray")).toBe(true);
|
||||||
|
|
||||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Highlight, 1);
|
|
@ -1,18 +1,18 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`RoomHeaderButtons-test.tsx should render 1`] = `
|
exports[`LegacyRoomHeaderButtons-test.tsx should render 1`] = `
|
||||||
<DocumentFragment>
|
<DocumentFragment>
|
||||||
<div
|
<div
|
||||||
aria-current="false"
|
aria-current="false"
|
||||||
aria-label="Chat"
|
aria-label="Chat"
|
||||||
class="mx_AccessibleButton mx_RoomHeader_button mx_RightPanel_timelineCardButton"
|
class="mx_AccessibleButton mx_LegacyRoomHeader_button mx_RightPanel_timelineCardButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
aria-current="false"
|
aria-current="false"
|
||||||
aria-label="Threads"
|
aria-label="Threads"
|
||||||
class="mx_AccessibleButton mx_RoomHeader_button mx_RightPanel_threadsButton"
|
class="mx_AccessibleButton mx_LegacyRoomHeader_button mx_RightPanel_threadsButton"
|
||||||
data-testid="threadsButton"
|
data-testid="threadsButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
|
@ -20,14 +20,14 @@ exports[`RoomHeaderButtons-test.tsx should render 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-current="false"
|
aria-current="false"
|
||||||
aria-label="Notifications"
|
aria-label="Notifications"
|
||||||
class="mx_AccessibleButton mx_RoomHeader_button mx_RightPanel_notifsButton"
|
class="mx_AccessibleButton mx_LegacyRoomHeader_button mx_RightPanel_notifsButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
aria-current="false"
|
aria-current="false"
|
||||||
aria-label="Room info"
|
aria-label="Room info"
|
||||||
class="mx_AccessibleButton mx_RoomHeader_button mx_RightPanel_roomSummaryButton"
|
class="mx_AccessibleButton mx_LegacyRoomHeader_button mx_RightPanel_roomSummaryButton"
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
/>
|
/>
|
884
test/components/views/rooms/LegacyRoomHeader-test.tsx
Normal file
884
test/components/views/rooms/LegacyRoomHeader-test.tsx
Normal file
|
@ -0,0 +1,884 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 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 from "react";
|
||||||
|
import { render, screen, act, fireEvent, waitFor, getByRole, RenderResult } from "@testing-library/react";
|
||||||
|
import { mocked, Mocked } from "jest-mock";
|
||||||
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
|
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import { ClientWidgetApi, Widget } from "matrix-widget-api";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
||||||
|
|
||||||
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import {
|
||||||
|
stubClient,
|
||||||
|
mkRoomMember,
|
||||||
|
setupAsyncStoreWithClient,
|
||||||
|
resetAsyncStoreWithClient,
|
||||||
|
mockPlatformPeg,
|
||||||
|
} from "../../../test-utils";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import RoomHeader, { IProps as RoomHeaderProps } from "../../../../src/components/views/rooms/LegacyRoomHeader";
|
||||||
|
import { SearchScope } from "../../../../src/components/views/rooms/SearchBar";
|
||||||
|
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
||||||
|
import { mkEvent } from "../../../test-utils";
|
||||||
|
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||||
|
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||||
|
import SdkConfig from "../../../../src/SdkConfig";
|
||||||
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
|
import { ElementCall, JitsiCall } from "../../../../src/models/Call";
|
||||||
|
import { CallStore } from "../../../../src/stores/CallStore";
|
||||||
|
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
|
||||||
|
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
|
import WidgetStore from "../../../../src/stores/WidgetStore";
|
||||||
|
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
|
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
||||||
|
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
|
||||||
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
|
||||||
|
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
||||||
|
import { UIComponent } from "../../../../src/settings/UIFeature";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
||||||
|
shouldShowComponent: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("LegacyRoomHeader", () => {
|
||||||
|
let client: Mocked<MatrixClient>;
|
||||||
|
let room: Room;
|
||||||
|
let alice: RoomMember;
|
||||||
|
let bob: RoomMember;
|
||||||
|
let carol: RoomMember;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockPlatformPeg({ supportsJitsiScreensharing: () => true });
|
||||||
|
|
||||||
|
stubClient();
|
||||||
|
client = mocked(MatrixClientPeg.safeGet());
|
||||||
|
client.getUserId.mockReturnValue("@alice:example.org");
|
||||||
|
|
||||||
|
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
room.currentState.setStateEvents([mkCreationEvent(room.roomId, "@alice:example.org")]);
|
||||||
|
|
||||||
|
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||||
|
client.getRooms.mockReturnValue([room]);
|
||||||
|
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||||
|
client.sendStateEvent.mockImplementation(async (roomId, eventType, content, stateKey = "") => {
|
||||||
|
if (roomId !== room.roomId) throw new Error("Unknown room");
|
||||||
|
const event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: eventType,
|
||||||
|
room: roomId,
|
||||||
|
user: alice.userId,
|
||||||
|
skey: stateKey,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
return { event_id: event.getId()! };
|
||||||
|
});
|
||||||
|
|
||||||
|
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||||
|
bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||||
|
carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||||
|
|
||||||
|
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
||||||
|
client.getRooms.mockReturnValue([room]);
|
||||||
|
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[CallStore.instance, WidgetStore.instance].map((store) => setupAsyncStoreWithClient(store, client)),
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||||
|
[MediaDeviceKindEnum.AudioInput]: [],
|
||||||
|
[MediaDeviceKindEnum.VideoInput]: [],
|
||||||
|
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
DMRoomMap.makeShared(client);
|
||||||
|
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(carol.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await Promise.all([CallStore.instance, WidgetStore.instance].map(resetAsyncStoreWithClient));
|
||||||
|
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
SdkConfig.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockRoomType = (type: string) => {
|
||||||
|
jest.spyOn(room, "getType").mockReturnValue(type);
|
||||||
|
};
|
||||||
|
const mockRoomMembers = (members: RoomMember[]) => {
|
||||||
|
jest.spyOn(room, "getJoinedMembers").mockReturnValue(members);
|
||||||
|
jest.spyOn(room, "getMember").mockImplementation(
|
||||||
|
(userId) => members.find((member) => member.userId === userId) ?? null,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const mockEnabledSettings = (settings: string[]) => {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settings.includes(settingName));
|
||||||
|
};
|
||||||
|
const mockEventPowerLevels = (events: { [eventType: string]: number }) => {
|
||||||
|
room.currentState.setStateEvents([
|
||||||
|
mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: EventType.RoomPowerLevels,
|
||||||
|
room: room.roomId,
|
||||||
|
user: alice.userId,
|
||||||
|
skey: "",
|
||||||
|
content: { events, state_default: 0 },
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
const mockLegacyCall = () => {
|
||||||
|
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue({} as unknown as MatrixCall);
|
||||||
|
};
|
||||||
|
const withCall = async (fn: (call: ElementCall) => void | Promise<void>): Promise<void> => {
|
||||||
|
await ElementCall.create(room);
|
||||||
|
const call = CallStore.instance.getCall(room.roomId);
|
||||||
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
|
const widget = new Widget(call.widget);
|
||||||
|
|
||||||
|
const eventEmitter = new EventEmitter();
|
||||||
|
const messaging = {
|
||||||
|
on: eventEmitter.on.bind(eventEmitter),
|
||||||
|
off: eventEmitter.off.bind(eventEmitter),
|
||||||
|
once: eventEmitter.once.bind(eventEmitter),
|
||||||
|
emit: eventEmitter.emit.bind(eventEmitter),
|
||||||
|
stop: jest.fn(),
|
||||||
|
transport: {
|
||||||
|
send: jest.fn(),
|
||||||
|
reply: jest.fn(),
|
||||||
|
},
|
||||||
|
} as unknown as Mocked<ClientWidgetApi>;
|
||||||
|
WidgetMessagingStore.instance.storeMessaging(widget, call.roomId, messaging);
|
||||||
|
|
||||||
|
await fn(call);
|
||||||
|
|
||||||
|
call.destroy();
|
||||||
|
WidgetMessagingStore.instance.stopMessaging(widget, call.roomId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHeader = (props: Partial<RoomHeaderProps> = {}, roomContext: Partial<IRoomState> = {}) => {
|
||||||
|
render(
|
||||||
|
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
||||||
|
<RoomHeader
|
||||||
|
room={room}
|
||||||
|
inRoom={true}
|
||||||
|
onSearchClick={() => {}}
|
||||||
|
onInviteClick={null}
|
||||||
|
onForgetClick={() => {}}
|
||||||
|
onAppsClick={() => {}}
|
||||||
|
e2eStatus={E2EStatus.Normal}
|
||||||
|
appsShown={true}
|
||||||
|
searchInfo={{
|
||||||
|
searchId: Math.random(),
|
||||||
|
promise: new Promise<ISearchResults>(() => {}),
|
||||||
|
term: "",
|
||||||
|
scope: SearchScope.Room,
|
||||||
|
count: 0,
|
||||||
|
}}
|
||||||
|
viewingCall={false}
|
||||||
|
activeCall={null}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</RoomContext.Provider>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
it("hides call buttons in video rooms", () => {
|
||||||
|
mockRoomType(RoomType.UnstableCall);
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_video_rooms", "feature_element_call_video_rooms"]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.queryByRole("button", { name: /call/i })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides call buttons if showCallButtonsInComposer is disabled", () => {
|
||||||
|
mockEnabledSettings([]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.queryByRole("button", { name: /call/i })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
|
||||||
|
"and there's an ongoing call",
|
||||||
|
async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
SdkConfig.put({
|
||||||
|
element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" },
|
||||||
|
});
|
||||||
|
await ElementCall.create(room);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"hides the voice call button and starts an Element call when the video call button is pressed if configured to " +
|
||||||
|
"use Element Call exclusively",
|
||||||
|
async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
SdkConfig.put({
|
||||||
|
element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" },
|
||||||
|
});
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
||||||
|
|
||||||
|
const dispatcherSpy = jest.fn();
|
||||||
|
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
defaultDispatcher.unregister(dispatcherRef);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
|
||||||
|
"and the user lacks permission",
|
||||||
|
() => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
SdkConfig.put({
|
||||||
|
element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" },
|
||||||
|
});
|
||||||
|
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("disables call buttons in the new group call experience if there's an ongoing Element call", async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
await ElementCall.create(room);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons in the new group call experience if there's an ongoing legacy 1:1 call", () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
mockLegacyCall();
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons in the new group call experience if there's an existing Jitsi widget", async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
await JitsiCall.create(room);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons in the new group call experience if there's no other members", () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
"starts a legacy 1:1 call when call buttons are pressed in the new group call experience if there's 1 other " +
|
||||||
|
"member",
|
||||||
|
async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
mockRoomMembers([alice, bob]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
|
||||||
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
|
placeCallSpy.mockClear();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"creates a Jitsi widget when call buttons are pressed in the new group call experience if the user lacks " +
|
||||||
|
"permission to start Element calls",
|
||||||
|
async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
mockRoomMembers([alice, bob, carol]);
|
||||||
|
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
|
||||||
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
|
placeCallSpy.mockClear();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"creates a Jitsi widget when the voice call button is pressed and shows a menu when the video call button is " +
|
||||||
|
"pressed in the new group call experience",
|
||||||
|
async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
mockRoomMembers([alice, bob, carol]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
|
||||||
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
|
// First try creating a Jitsi widget from the menu
|
||||||
|
placeCallSpy.mockClear();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
fireEvent.click(getByRole(screen.getByRole("menu"), "menuitem", { name: /jitsi/i }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
||||||
|
|
||||||
|
// Then try starting an Element call from the menu
|
||||||
|
const dispatcherSpy = jest.fn();
|
||||||
|
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
fireEvent.click(getByRole(screen.getByRole("menu"), "menuitem", { name: /element/i }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
defaultDispatcher.unregister(dispatcherRef);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it(
|
||||||
|
"disables the voice call button and starts an Element call when the video call button is pressed in the new " +
|
||||||
|
"group call experience if the user lacks permission to edit widgets",
|
||||||
|
async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
mockRoomMembers([alice, bob, carol]);
|
||||||
|
mockEventPowerLevels({ "im.vector.modular.widgets": 100 });
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
|
const dispatcherSpy = jest.fn();
|
||||||
|
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
defaultDispatcher.unregister(dispatcherRef);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("disables call buttons in the new group call experience if the user lacks permission", () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
||||||
|
mockRoomMembers([alice, bob, carol]);
|
||||||
|
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100, "im.vector.modular.widgets": 100 });
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons if there's an ongoing legacy 1:1 call", () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer"]);
|
||||||
|
mockLegacyCall();
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons if there's an existing Jitsi widget", async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer"]);
|
||||||
|
await JitsiCall.create(room);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons if there's no other members", () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer"]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("starts a legacy 1:1 call when call buttons are pressed if there's 1 other member", async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer"]);
|
||||||
|
mockRoomMembers([alice, bob]);
|
||||||
|
mockEventPowerLevels({ "im.vector.modular.widgets": 100 }); // Just to verify that it doesn't try to use Jitsi
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
|
||||||
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
|
placeCallSpy.mockClear();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates a Jitsi widget when call buttons are pressed", async () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer"]);
|
||||||
|
mockRoomMembers([alice, bob, carol]);
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
|
||||||
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
|
placeCallSpy.mockClear();
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
||||||
|
await act(() => Promise.resolve()); // Allow effects to settle
|
||||||
|
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("disables call buttons if the user lacks permission", () => {
|
||||||
|
mockEnabledSettings(["showCallButtonsInComposer"]);
|
||||||
|
mockRoomMembers([alice, bob, carol]);
|
||||||
|
mockEventPowerLevels({ "im.vector.modular.widgets": 100 });
|
||||||
|
|
||||||
|
renderHeader();
|
||||||
|
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a close button when viewing a call lobby that returns to the timeline when pressed", async () => {
|
||||||
|
mockEnabledSettings(["feature_group_calls"]);
|
||||||
|
|
||||||
|
renderHeader({ viewingCall: true });
|
||||||
|
|
||||||
|
const dispatcherSpy = jest.fn();
|
||||||
|
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /close/i }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
defaultDispatcher.unregister(dispatcherRef);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a reduce button when viewing a call that returns to the timeline when pressed", async () => {
|
||||||
|
mockEnabledSettings(["feature_group_calls"]);
|
||||||
|
|
||||||
|
await withCall(async (call) => {
|
||||||
|
renderHeader({ viewingCall: true, activeCall: call });
|
||||||
|
|
||||||
|
const dispatcherSpy = jest.fn();
|
||||||
|
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /timeline/i }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(dispatcherSpy).toHaveBeenCalledWith({
|
||||||
|
action: Action.ViewRoom,
|
||||||
|
room_id: room.roomId,
|
||||||
|
view_call: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
defaultDispatcher.unregister(dispatcherRef);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a layout button when viewing a call that shows a menu when pressed", async () => {
|
||||||
|
mockEnabledSettings(["feature_group_calls"]);
|
||||||
|
|
||||||
|
await withCall(async (call) => {
|
||||||
|
await call.connect();
|
||||||
|
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget))!;
|
||||||
|
renderHeader({ viewingCall: true, activeCall: call });
|
||||||
|
|
||||||
|
// Should start with Freedom selected
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
|
||||||
|
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });
|
||||||
|
|
||||||
|
// Clicking Spotlight should tell the widget to switch and close the menu
|
||||||
|
fireEvent.click(screen.getByRole("menuitemradio", { name: "Spotlight" }));
|
||||||
|
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
|
||||||
|
expect(screen.queryByRole("menu")).toBeNull();
|
||||||
|
|
||||||
|
// When the widget responds and the user reopens the menu, they should see Spotlight selected
|
||||||
|
act(() => {
|
||||||
|
messaging.emit(
|
||||||
|
`action:${ElementWidgetActions.SpotlightLayout}`,
|
||||||
|
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
|
||||||
|
screen.getByRole("menuitemradio", { name: "Spotlight", checked: true });
|
||||||
|
|
||||||
|
// Now try switching back to Freedom
|
||||||
|
fireEvent.click(screen.getByRole("menuitemradio", { name: "Freedom" }));
|
||||||
|
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
|
||||||
|
expect(screen.queryByRole("menu")).toBeNull();
|
||||||
|
|
||||||
|
// When the widget responds and the user reopens the menu, they should see Freedom selected
|
||||||
|
act(() => {
|
||||||
|
messaging.emit(
|
||||||
|
`action:${ElementWidgetActions.TileLayout}`,
|
||||||
|
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
|
||||||
|
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an invite button in video rooms", () => {
|
||||||
|
mockEnabledSettings(["feature_video_rooms", "feature_element_call_video_rooms"]);
|
||||||
|
mockRoomType(RoomType.UnstableCall);
|
||||||
|
|
||||||
|
const onInviteClick = jest.fn();
|
||||||
|
renderHeader({ onInviteClick, viewingCall: true });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /invite/i }));
|
||||||
|
expect(onInviteClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides the invite button in non-video rooms when viewing a call", () => {
|
||||||
|
renderHeader({ onInviteClick: () => {}, viewingCall: true });
|
||||||
|
|
||||||
|
expect(screen.queryByRole("button", { name: /invite/i })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the room avatar in a room with only ourselves", () => {
|
||||||
|
// When we render a non-DM room with 1 person in it
|
||||||
|
const room = createRoom({ name: "X Room", isDm: false, userIds: [] });
|
||||||
|
const rendered = mountHeader(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
||||||
|
expect(initial).toHaveTextContent("X");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
||||||
|
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the room avatar in a room with 2 people", () => {
|
||||||
|
// When we render a non-DM room with 2 people in it
|
||||||
|
const room = createRoom({ name: "Y Room", isDm: false, userIds: ["other"] });
|
||||||
|
const rendered = mountHeader(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
||||||
|
expect(initial).toHaveTextContent("Y");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
||||||
|
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the room avatar in a room with >2 people", () => {
|
||||||
|
// When we render a non-DM room with 3 people in it
|
||||||
|
const room = createRoom({ name: "Z Room", isDm: false, userIds: ["other1", "other2"] });
|
||||||
|
const rendered = mountHeader(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
||||||
|
expect(initial).toHaveTextContent("Z");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
||||||
|
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the room avatar in a DM with only ourselves", () => {
|
||||||
|
// When we render a non-DM room with 1 person in it
|
||||||
|
const room = createRoom({ name: "Z Room", isDm: true, userIds: [] });
|
||||||
|
const rendered = mountHeader(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
||||||
|
expect(initial).toHaveTextContent("Z");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
||||||
|
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the user avatar in a DM with 2 people", () => {
|
||||||
|
// Note: this is the interesting case - this is the ONLY
|
||||||
|
// time we should use the user's avatar.
|
||||||
|
|
||||||
|
// When we render a DM room with only 2 people in it
|
||||||
|
const room = createRoom({ name: "Y Room", isDm: true, userIds: ["other"] });
|
||||||
|
const rendered = mountHeader(room);
|
||||||
|
|
||||||
|
// Then we use the other user's avatar as our room's image avatar
|
||||||
|
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
||||||
|
expect(image).toHaveAttribute("src", "http://this.is.a.url/example.org/other");
|
||||||
|
|
||||||
|
// And there is no initial avatar
|
||||||
|
expect(rendered.container.querySelector(".mx_BaseAvatar_initial")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows the room avatar in a DM with >2 people", () => {
|
||||||
|
// When we render a DM room with 3 people in it
|
||||||
|
const room = createRoom({
|
||||||
|
name: "Z Room",
|
||||||
|
isDm: true,
|
||||||
|
userIds: ["other1", "other2"],
|
||||||
|
});
|
||||||
|
const rendered = mountHeader(room);
|
||||||
|
|
||||||
|
// Then the room's avatar is the initial of its name
|
||||||
|
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
||||||
|
expect(initial).toHaveTextContent("Z");
|
||||||
|
|
||||||
|
// And there is no image avatar (because it's not set on this room)
|
||||||
|
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
||||||
|
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders call buttons normally", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: ["other"] });
|
||||||
|
const wrapper = mountHeader(room);
|
||||||
|
|
||||||
|
expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeDefined();
|
||||||
|
expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hides call buttons when the room is tombstoned", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = mountHeader(
|
||||||
|
room,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
tombstone: mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.tombstone",
|
||||||
|
room: room.roomId,
|
||||||
|
user: "@user1:server",
|
||||||
|
skey: "",
|
||||||
|
content: {},
|
||||||
|
ts: Date.now(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeFalsy();
|
||||||
|
expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render buttons if not passing showButtons (default true)", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = mountHeader(room);
|
||||||
|
expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_button")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render buttons if passing showButtons = false", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = mountHeader(room, { showButtons: false });
|
||||||
|
expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_button")).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true) and UIComponent customisations room options enabled", () => {
|
||||||
|
mocked(shouldShowComponent).mockReturnValue(true);
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = mountHeader(room);
|
||||||
|
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu);
|
||||||
|
expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_name.mx_AccessibleButton")).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
[false, true],
|
||||||
|
[true, false],
|
||||||
|
])(
|
||||||
|
"should not render the room options context menu if passing enableRoomOptionsMenu = %s and UIComponent customisations room options enable = %s",
|
||||||
|
(enableRoomOptionsMenu, showRoomOptionsMenu) => {
|
||||||
|
mocked(shouldShowComponent).mockReturnValue(showRoomOptionsMenu);
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = mountHeader(room, { enableRoomOptionsMenu });
|
||||||
|
expect(wrapper.container.querySelector(".mx_LegacyRoomHeader_name.mx_AccessibleButton")).toBeFalsy();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface IRoomCreationInfo {
|
||||||
|
name: string;
|
||||||
|
isDm: boolean;
|
||||||
|
userIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function createRoom(info: IRoomCreationInfo) {
|
||||||
|
stubClient();
|
||||||
|
const client: MatrixClient = MatrixClientPeg.safeGet();
|
||||||
|
|
||||||
|
const roomId = "!1234567890:domain";
|
||||||
|
const userId = client.getUserId()!;
|
||||||
|
if (info.isDm) {
|
||||||
|
client.getAccountData = (eventType) => {
|
||||||
|
expect(eventType).toEqual("m.direct");
|
||||||
|
return mkDirectEvent(roomId, userId, info.userIds);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
DMRoomMap.makeShared(client).start();
|
||||||
|
|
||||||
|
const room = new Room(roomId, client, userId, {
|
||||||
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
|
});
|
||||||
|
|
||||||
|
const otherJoinEvents: MatrixEvent[] = [];
|
||||||
|
for (const otherUserId of info.userIds) {
|
||||||
|
otherJoinEvents.push(mkJoinEvent(roomId, otherUserId));
|
||||||
|
}
|
||||||
|
|
||||||
|
room.currentState.setStateEvents([
|
||||||
|
mkCreationEvent(roomId, userId),
|
||||||
|
mkNameEvent(roomId, userId, info.name),
|
||||||
|
mkJoinEvent(roomId, userId),
|
||||||
|
...otherJoinEvents,
|
||||||
|
]);
|
||||||
|
room.recalculate();
|
||||||
|
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): RenderResult {
|
||||||
|
const props: RoomHeaderProps = {
|
||||||
|
room,
|
||||||
|
inRoom: true,
|
||||||
|
onSearchClick: () => {},
|
||||||
|
onInviteClick: null,
|
||||||
|
onForgetClick: () => {},
|
||||||
|
onAppsClick: () => {},
|
||||||
|
e2eStatus: E2EStatus.Normal,
|
||||||
|
appsShown: true,
|
||||||
|
searchInfo: {
|
||||||
|
searchId: Math.random(),
|
||||||
|
promise: new Promise<ISearchResults>(() => {}),
|
||||||
|
term: "",
|
||||||
|
scope: SearchScope.Room,
|
||||||
|
count: 0,
|
||||||
|
},
|
||||||
|
viewingCall: false,
|
||||||
|
activeCall: null,
|
||||||
|
...propsOverride,
|
||||||
|
};
|
||||||
|
|
||||||
|
return render(
|
||||||
|
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
||||||
|
<RoomHeader {...props} />
|
||||||
|
</RoomContext.Provider>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkCreationEvent(roomId: string, userId: string): MatrixEvent {
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.create",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: {
|
||||||
|
creator: userId,
|
||||||
|
room_version: "5",
|
||||||
|
predecessor: {
|
||||||
|
room_id: "!prevroom",
|
||||||
|
event_id: "$someevent",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkNameEvent(roomId: string, userId: string, name: string): MatrixEvent {
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.name",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: { name },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkJoinEvent(roomId: string, userId: string) {
|
||||||
|
const ret = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.member",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content: {
|
||||||
|
membership: "join",
|
||||||
|
avatar_url: "mxc://example.org/" + userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
ret.event.state_key = userId;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mkDirectEvent(roomId: string, userId: string, otherUsers: string[]): MatrixEvent {
|
||||||
|
const content: Record<string, string[]> = {};
|
||||||
|
for (const otherUserId of otherUsers) {
|
||||||
|
content[otherUserId] = [roomId];
|
||||||
|
}
|
||||||
|
return mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.direct",
|
||||||
|
room: roomId,
|
||||||
|
user: userId,
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,870 +15,32 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render, screen, act, fireEvent, waitFor, getByRole, RenderResult } from "@testing-library/react";
|
import { Mocked } from "jest-mock";
|
||||||
import { mocked, Mocked } from "jest-mock";
|
import { render } from "@testing-library/react";
|
||||||
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
|
||||||
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
|
||||||
import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
import { ClientWidgetApi, Widget } from "matrix-widget-api";
|
|
||||||
import EventEmitter from "events";
|
|
||||||
import { ISearchResults } from "matrix-js-sdk/src/@types/search";
|
|
||||||
|
|
||||||
|
import { stubClient } from "../../../test-utils";
|
||||||
|
import RoomHeader from "../../../../src/components/views/rooms/RoomHeader";
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
|
||||||
import type { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
|
||||||
import {
|
|
||||||
stubClient,
|
|
||||||
mkRoomMember,
|
|
||||||
setupAsyncStoreWithClient,
|
|
||||||
resetAsyncStoreWithClient,
|
|
||||||
mockPlatformPeg,
|
|
||||||
} from "../../../test-utils";
|
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
|
||||||
import RoomHeader, { IProps as RoomHeaderProps } from "../../../../src/components/views/rooms/RoomHeader";
|
|
||||||
import { SearchScope } from "../../../../src/components/views/rooms/SearchBar";
|
|
||||||
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
|
||||||
import { mkEvent } from "../../../test-utils";
|
|
||||||
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
|
||||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
|
||||||
import SdkConfig from "../../../../src/SdkConfig";
|
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
|
||||||
import { ElementCall, JitsiCall } from "../../../../src/models/Call";
|
|
||||||
import { CallStore } from "../../../../src/stores/CallStore";
|
|
||||||
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
|
|
||||||
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
|
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
|
||||||
import WidgetStore from "../../../../src/stores/WidgetStore";
|
|
||||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
|
||||||
import WidgetUtils from "../../../../src/utils/WidgetUtils";
|
|
||||||
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
|
|
||||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
|
|
||||||
import { shouldShowComponent } from "../../../../src/customisations/helpers/UIComponents";
|
|
||||||
import { UIComponent } from "../../../../src/settings/UIFeature";
|
|
||||||
|
|
||||||
jest.mock("../../../../src/customisations/helpers/UIComponents", () => ({
|
describe("Roomeader", () => {
|
||||||
shouldShowComponent: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("RoomHeader", () => {
|
|
||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let alice: RoomMember;
|
|
||||||
let bob: RoomMember;
|
const ROOM_ID = "!1:example.org";
|
||||||
let carol: RoomMember;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mockPlatformPeg({ supportsJitsiScreensharing: () => true });
|
|
||||||
|
|
||||||
stubClient();
|
stubClient();
|
||||||
client = mocked(MatrixClientPeg.safeGet());
|
room = new Room(ROOM_ID, client, "@alice:example.org");
|
||||||
client.getUserId.mockReturnValue("@alice:example.org");
|
|
||||||
|
|
||||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
||||||
});
|
|
||||||
room.currentState.setStateEvents([mkCreationEvent(room.roomId, "@alice:example.org")]);
|
|
||||||
|
|
||||||
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
|
||||||
client.getRooms.mockReturnValue([room]);
|
|
||||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
|
||||||
client.sendStateEvent.mockImplementation(async (roomId, eventType, content, stateKey = "") => {
|
|
||||||
if (roomId !== room.roomId) throw new Error("Unknown room");
|
|
||||||
const event = mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: eventType,
|
|
||||||
room: roomId,
|
|
||||||
user: alice.userId,
|
|
||||||
skey: stateKey,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
room.addLiveEvents([event]);
|
|
||||||
return { event_id: event.getId()! };
|
|
||||||
});
|
|
||||||
|
|
||||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
|
||||||
bob = mkRoomMember(room.roomId, "@bob:example.org");
|
|
||||||
carol = mkRoomMember(room.roomId, "@carol:example.org");
|
|
||||||
|
|
||||||
client.getRoom.mockImplementation((roomId) => (roomId === room.roomId ? room : null));
|
|
||||||
client.getRooms.mockReturnValue([room]);
|
|
||||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
[CallStore.instance, WidgetStore.instance].map((store) => setupAsyncStoreWithClient(store, client)),
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
|
||||||
[MediaDeviceKindEnum.AudioInput]: [],
|
|
||||||
[MediaDeviceKindEnum.VideoInput]: [],
|
|
||||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
DMRoomMap.makeShared(client);
|
|
||||||
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(carol.userId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
it("renders with no props", () => {
|
||||||
await Promise.all([CallStore.instance, WidgetStore.instance].map(resetAsyncStoreWithClient));
|
const { asFragment } = render(<RoomHeader />);
|
||||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
expect(asFragment()).toMatchSnapshot();
|
||||||
jest.restoreAllMocks();
|
|
||||||
SdkConfig.reset();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockRoomType = (type: string) => {
|
it("renders the room header", () => {
|
||||||
jest.spyOn(room, "getType").mockReturnValue(type);
|
const { container } = render(<RoomHeader room={room} />);
|
||||||
};
|
expect(container).toHaveTextContent(ROOM_ID);
|
||||||
const mockRoomMembers = (members: RoomMember[]) => {
|
|
||||||
jest.spyOn(room, "getJoinedMembers").mockReturnValue(members);
|
|
||||||
jest.spyOn(room, "getMember").mockImplementation(
|
|
||||||
(userId) => members.find((member) => member.userId === userId) ?? null,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const mockEnabledSettings = (settings: string[]) => {
|
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => settings.includes(settingName));
|
|
||||||
};
|
|
||||||
const mockEventPowerLevels = (events: { [eventType: string]: number }) => {
|
|
||||||
room.currentState.setStateEvents([
|
|
||||||
mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: EventType.RoomPowerLevels,
|
|
||||||
room: room.roomId,
|
|
||||||
user: alice.userId,
|
|
||||||
skey: "",
|
|
||||||
content: { events, state_default: 0 },
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
const mockLegacyCall = () => {
|
|
||||||
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue({} as unknown as MatrixCall);
|
|
||||||
};
|
|
||||||
const withCall = async (fn: (call: ElementCall) => void | Promise<void>): Promise<void> => {
|
|
||||||
await ElementCall.create(room);
|
|
||||||
const call = CallStore.instance.getCall(room.roomId);
|
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
|
||||||
|
|
||||||
const widget = new Widget(call.widget);
|
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const messaging = {
|
|
||||||
on: eventEmitter.on.bind(eventEmitter),
|
|
||||||
off: eventEmitter.off.bind(eventEmitter),
|
|
||||||
once: eventEmitter.once.bind(eventEmitter),
|
|
||||||
emit: eventEmitter.emit.bind(eventEmitter),
|
|
||||||
stop: jest.fn(),
|
|
||||||
transport: {
|
|
||||||
send: jest.fn(),
|
|
||||||
reply: jest.fn(),
|
|
||||||
},
|
|
||||||
} as unknown as Mocked<ClientWidgetApi>;
|
|
||||||
WidgetMessagingStore.instance.storeMessaging(widget, call.roomId, messaging);
|
|
||||||
|
|
||||||
await fn(call);
|
|
||||||
|
|
||||||
call.destroy();
|
|
||||||
WidgetMessagingStore.instance.stopMessaging(widget, call.roomId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderHeader = (props: Partial<RoomHeaderProps> = {}, roomContext: Partial<IRoomState> = {}) => {
|
|
||||||
render(
|
|
||||||
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
|
||||||
<RoomHeader
|
|
||||||
room={room}
|
|
||||||
inRoom={true}
|
|
||||||
onSearchClick={() => {}}
|
|
||||||
onInviteClick={null}
|
|
||||||
onForgetClick={() => {}}
|
|
||||||
onAppsClick={() => {}}
|
|
||||||
e2eStatus={E2EStatus.Normal}
|
|
||||||
appsShown={true}
|
|
||||||
searchInfo={{
|
|
||||||
searchId: Math.random(),
|
|
||||||
promise: new Promise<ISearchResults>(() => {}),
|
|
||||||
term: "",
|
|
||||||
scope: SearchScope.Room,
|
|
||||||
count: 0,
|
|
||||||
}}
|
|
||||||
viewingCall={false}
|
|
||||||
activeCall={null}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</RoomContext.Provider>,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
it("hides call buttons in video rooms", () => {
|
|
||||||
mockRoomType(RoomType.UnstableCall);
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_video_rooms", "feature_element_call_video_rooms"]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.queryByRole("button", { name: /call/i })).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("hides call buttons if showCallButtonsInComposer is disabled", () => {
|
|
||||||
mockEnabledSettings([]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.queryByRole("button", { name: /call/i })).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it(
|
|
||||||
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
|
|
||||||
"and there's an ongoing call",
|
|
||||||
async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
SdkConfig.put({
|
|
||||||
element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" },
|
|
||||||
});
|
|
||||||
await ElementCall.create(room);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
|
||||||
"hides the voice call button and starts an Element call when the video call button is pressed if configured to " +
|
|
||||||
"use Element Call exclusively",
|
|
||||||
async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
SdkConfig.put({
|
|
||||||
element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" },
|
|
||||||
});
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
|
||||||
|
|
||||||
const dispatcherSpy = jest.fn();
|
|
||||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
view_call: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
defaultDispatcher.unregister(dispatcherRef);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
|
||||||
"hides the voice call button and disables the video call button if configured to use Element Call exclusively " +
|
|
||||||
"and the user lacks permission",
|
|
||||||
() => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
SdkConfig.put({
|
|
||||||
element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" },
|
|
||||||
});
|
|
||||||
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("disables call buttons in the new group call experience if there's an ongoing Element call", async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
await ElementCall.create(room);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons in the new group call experience if there's an ongoing legacy 1:1 call", () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
mockLegacyCall();
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons in the new group call experience if there's an existing Jitsi widget", async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
await JitsiCall.create(room);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons in the new group call experience if there's no other members", () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it(
|
|
||||||
"starts a legacy 1:1 call when call buttons are pressed in the new group call experience if there's 1 other " +
|
|
||||||
"member",
|
|
||||||
async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
mockRoomMembers([alice, bob]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
|
||||||
|
|
||||||
placeCallSpy.mockClear();
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
|
||||||
"creates a Jitsi widget when call buttons are pressed in the new group call experience if the user lacks " +
|
|
||||||
"permission to start Element calls",
|
|
||||||
async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
mockRoomMembers([alice, bob, carol]);
|
|
||||||
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
|
||||||
|
|
||||||
placeCallSpy.mockClear();
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
|
||||||
"creates a Jitsi widget when the voice call button is pressed and shows a menu when the video call button is " +
|
|
||||||
"pressed in the new group call experience",
|
|
||||||
async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
mockRoomMembers([alice, bob, carol]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
|
||||||
|
|
||||||
// First try creating a Jitsi widget from the menu
|
|
||||||
placeCallSpy.mockClear();
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
fireEvent.click(getByRole(screen.getByRole("menu"), "menuitem", { name: /jitsi/i }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
|
||||||
|
|
||||||
// Then try starting an Element call from the menu
|
|
||||||
const dispatcherSpy = jest.fn();
|
|
||||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
fireEvent.click(getByRole(screen.getByRole("menu"), "menuitem", { name: /element/i }));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
view_call: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
defaultDispatcher.unregister(dispatcherRef);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
|
||||||
"disables the voice call button and starts an Element call when the video call button is pressed in the new " +
|
|
||||||
"group call experience if the user lacks permission to edit widgets",
|
|
||||||
async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
mockRoomMembers([alice, bob, carol]);
|
|
||||||
mockEventPowerLevels({ "im.vector.modular.widgets": 100 });
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
|
|
||||||
const dispatcherSpy = jest.fn();
|
|
||||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
view_call: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
defaultDispatcher.unregister(dispatcherRef);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
it("disables call buttons in the new group call experience if the user lacks permission", () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
|
|
||||||
mockRoomMembers([alice, bob, carol]);
|
|
||||||
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100, "im.vector.modular.widgets": 100 });
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons if there's an ongoing legacy 1:1 call", () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer"]);
|
|
||||||
mockLegacyCall();
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons if there's an existing Jitsi widget", async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer"]);
|
|
||||||
await JitsiCall.create(room);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons if there's no other members", () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer"]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("starts a legacy 1:1 call when call buttons are pressed if there's 1 other member", async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer"]);
|
|
||||||
mockRoomMembers([alice, bob]);
|
|
||||||
mockEventPowerLevels({ "im.vector.modular.widgets": 100 }); // Just to verify that it doesn't try to use Jitsi
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
|
||||||
|
|
||||||
placeCallSpy.mockClear();
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a Jitsi widget when call buttons are pressed", async () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer"]);
|
|
||||||
mockRoomMembers([alice, bob, carol]);
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall").mockResolvedValue(undefined);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Voice call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Voice);
|
|
||||||
|
|
||||||
placeCallSpy.mockClear();
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "Video call" }));
|
|
||||||
await act(() => Promise.resolve()); // Allow effects to settle
|
|
||||||
expect(placeCallSpy).toHaveBeenCalledWith(room.roomId, CallType.Video);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables call buttons if the user lacks permission", () => {
|
|
||||||
mockEnabledSettings(["showCallButtonsInComposer"]);
|
|
||||||
mockRoomMembers([alice, bob, carol]);
|
|
||||||
mockEventPowerLevels({ "im.vector.modular.widgets": 100 });
|
|
||||||
|
|
||||||
renderHeader();
|
|
||||||
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows a close button when viewing a call lobby that returns to the timeline when pressed", async () => {
|
|
||||||
mockEnabledSettings(["feature_group_calls"]);
|
|
||||||
|
|
||||||
renderHeader({ viewingCall: true });
|
|
||||||
|
|
||||||
const dispatcherSpy = jest.fn();
|
|
||||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /close/i }));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
view_call: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
defaultDispatcher.unregister(dispatcherRef);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows a reduce button when viewing a call that returns to the timeline when pressed", async () => {
|
|
||||||
mockEnabledSettings(["feature_group_calls"]);
|
|
||||||
|
|
||||||
await withCall(async (call) => {
|
|
||||||
renderHeader({ viewingCall: true, activeCall: call });
|
|
||||||
|
|
||||||
const dispatcherSpy = jest.fn();
|
|
||||||
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /timeline/i }));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith({
|
|
||||||
action: Action.ViewRoom,
|
|
||||||
room_id: room.roomId,
|
|
||||||
view_call: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
defaultDispatcher.unregister(dispatcherRef);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows a layout button when viewing a call that shows a menu when pressed", async () => {
|
|
||||||
mockEnabledSettings(["feature_group_calls"]);
|
|
||||||
|
|
||||||
await withCall(async (call) => {
|
|
||||||
await call.connect();
|
|
||||||
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget))!;
|
|
||||||
renderHeader({ viewingCall: true, activeCall: call });
|
|
||||||
|
|
||||||
// Should start with Freedom selected
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
|
|
||||||
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });
|
|
||||||
|
|
||||||
// Clicking Spotlight should tell the widget to switch and close the menu
|
|
||||||
fireEvent.click(screen.getByRole("menuitemradio", { name: "Spotlight" }));
|
|
||||||
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
|
|
||||||
expect(screen.queryByRole("menu")).toBeNull();
|
|
||||||
|
|
||||||
// When the widget responds and the user reopens the menu, they should see Spotlight selected
|
|
||||||
act(() => {
|
|
||||||
messaging.emit(
|
|
||||||
`action:${ElementWidgetActions.SpotlightLayout}`,
|
|
||||||
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
|
|
||||||
screen.getByRole("menuitemradio", { name: "Spotlight", checked: true });
|
|
||||||
|
|
||||||
// Now try switching back to Freedom
|
|
||||||
fireEvent.click(screen.getByRole("menuitemradio", { name: "Freedom" }));
|
|
||||||
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
|
|
||||||
expect(screen.queryByRole("menu")).toBeNull();
|
|
||||||
|
|
||||||
// When the widget responds and the user reopens the menu, they should see Freedom selected
|
|
||||||
act(() => {
|
|
||||||
messaging.emit(
|
|
||||||
`action:${ElementWidgetActions.TileLayout}`,
|
|
||||||
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
|
|
||||||
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows an invite button in video rooms", () => {
|
|
||||||
mockEnabledSettings(["feature_video_rooms", "feature_element_call_video_rooms"]);
|
|
||||||
mockRoomType(RoomType.UnstableCall);
|
|
||||||
|
|
||||||
const onInviteClick = jest.fn();
|
|
||||||
renderHeader({ onInviteClick, viewingCall: true });
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /invite/i }));
|
|
||||||
expect(onInviteClick).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides the invite button in non-video rooms when viewing a call", () => {
|
|
||||||
renderHeader({ onInviteClick: () => {}, viewingCall: true });
|
|
||||||
|
|
||||||
expect(screen.queryByRole("button", { name: /invite/i })).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the room avatar in a room with only ourselves", () => {
|
|
||||||
// When we render a non-DM room with 1 person in it
|
|
||||||
const room = createRoom({ name: "X Room", isDm: false, userIds: [] });
|
|
||||||
const rendered = mountHeader(room);
|
|
||||||
|
|
||||||
// Then the room's avatar is the initial of its name
|
|
||||||
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
|
||||||
expect(initial).toHaveTextContent("X");
|
|
||||||
|
|
||||||
// And there is no image avatar (because it's not set on this room)
|
|
||||||
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
|
||||||
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the room avatar in a room with 2 people", () => {
|
|
||||||
// When we render a non-DM room with 2 people in it
|
|
||||||
const room = createRoom({ name: "Y Room", isDm: false, userIds: ["other"] });
|
|
||||||
const rendered = mountHeader(room);
|
|
||||||
|
|
||||||
// Then the room's avatar is the initial of its name
|
|
||||||
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
|
||||||
expect(initial).toHaveTextContent("Y");
|
|
||||||
|
|
||||||
// And there is no image avatar (because it's not set on this room)
|
|
||||||
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
|
||||||
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the room avatar in a room with >2 people", () => {
|
|
||||||
// When we render a non-DM room with 3 people in it
|
|
||||||
const room = createRoom({ name: "Z Room", isDm: false, userIds: ["other1", "other2"] });
|
|
||||||
const rendered = mountHeader(room);
|
|
||||||
|
|
||||||
// Then the room's avatar is the initial of its name
|
|
||||||
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
|
||||||
expect(initial).toHaveTextContent("Z");
|
|
||||||
|
|
||||||
// And there is no image avatar (because it's not set on this room)
|
|
||||||
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
|
||||||
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the room avatar in a DM with only ourselves", () => {
|
|
||||||
// When we render a non-DM room with 1 person in it
|
|
||||||
const room = createRoom({ name: "Z Room", isDm: true, userIds: [] });
|
|
||||||
const rendered = mountHeader(room);
|
|
||||||
|
|
||||||
// Then the room's avatar is the initial of its name
|
|
||||||
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
|
||||||
expect(initial).toHaveTextContent("Z");
|
|
||||||
|
|
||||||
// And there is no image avatar (because it's not set on this room)
|
|
||||||
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
|
||||||
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the user avatar in a DM with 2 people", () => {
|
|
||||||
// Note: this is the interesting case - this is the ONLY
|
|
||||||
// time we should use the user's avatar.
|
|
||||||
|
|
||||||
// When we render a DM room with only 2 people in it
|
|
||||||
const room = createRoom({ name: "Y Room", isDm: true, userIds: ["other"] });
|
|
||||||
const rendered = mountHeader(room);
|
|
||||||
|
|
||||||
// Then we use the other user's avatar as our room's image avatar
|
|
||||||
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
|
||||||
expect(image).toHaveAttribute("src", "http://this.is.a.url/example.org/other");
|
|
||||||
|
|
||||||
// And there is no initial avatar
|
|
||||||
expect(rendered.container.querySelector(".mx_BaseAvatar_initial")).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the room avatar in a DM with >2 people", () => {
|
|
||||||
// When we render a DM room with 3 people in it
|
|
||||||
const room = createRoom({
|
|
||||||
name: "Z Room",
|
|
||||||
isDm: true,
|
|
||||||
userIds: ["other1", "other2"],
|
|
||||||
});
|
|
||||||
const rendered = mountHeader(room);
|
|
||||||
|
|
||||||
// Then the room's avatar is the initial of its name
|
|
||||||
const initial = rendered.container.querySelector(".mx_BaseAvatar_initial");
|
|
||||||
expect(initial).toHaveTextContent("Z");
|
|
||||||
|
|
||||||
// And there is no image avatar (because it's not set on this room)
|
|
||||||
const image = rendered.container.querySelector(".mx_BaseAvatar_image");
|
|
||||||
expect(image).toHaveAttribute("src", "data:image/png;base64,00");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders call buttons normally", () => {
|
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: ["other"] });
|
|
||||||
const wrapper = mountHeader(room);
|
|
||||||
|
|
||||||
expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeDefined();
|
|
||||||
expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides call buttons when the room is tombstoned", () => {
|
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
|
||||||
const wrapper = mountHeader(
|
|
||||||
room,
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
tombstone: mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: "m.room.tombstone",
|
|
||||||
room: room.roomId,
|
|
||||||
user: "@user1:server",
|
|
||||||
skey: "",
|
|
||||||
content: {},
|
|
||||||
ts: Date.now(),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(wrapper.container.querySelector('[aria-label="Voice call"]')).toBeFalsy();
|
|
||||||
expect(wrapper.container.querySelector('[aria-label="Video call"]')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render buttons if not passing showButtons (default true)", () => {
|
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
|
||||||
const wrapper = mountHeader(room);
|
|
||||||
expect(wrapper.container.querySelector(".mx_RoomHeader_button")).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should not render buttons if passing showButtons = false", () => {
|
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
|
||||||
const wrapper = mountHeader(room, { showButtons: false });
|
|
||||||
expect(wrapper.container.querySelector(".mx_RoomHeader_button")).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true) and UIComponent customisations room options enabled", () => {
|
|
||||||
mocked(shouldShowComponent).mockReturnValue(true);
|
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
|
||||||
const wrapper = mountHeader(room);
|
|
||||||
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.RoomOptionsMenu);
|
|
||||||
expect(wrapper.container.querySelector(".mx_RoomHeader_name.mx_AccessibleButton")).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each([
|
|
||||||
[false, true],
|
|
||||||
[true, false],
|
|
||||||
])(
|
|
||||||
"should not render the room options context menu if passing enableRoomOptionsMenu = %s and UIComponent customisations room options enable = %s",
|
|
||||||
(enableRoomOptionsMenu, showRoomOptionsMenu) => {
|
|
||||||
mocked(shouldShowComponent).mockReturnValue(showRoomOptionsMenu);
|
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
|
||||||
const wrapper = mountHeader(room, { enableRoomOptionsMenu });
|
|
||||||
expect(wrapper.container.querySelector(".mx_RoomHeader_name.mx_AccessibleButton")).toBeFalsy();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IRoomCreationInfo {
|
|
||||||
name: string;
|
|
||||||
isDm: boolean;
|
|
||||||
userIds: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
function createRoom(info: IRoomCreationInfo) {
|
|
||||||
stubClient();
|
|
||||||
const client: MatrixClient = MatrixClientPeg.safeGet();
|
|
||||||
|
|
||||||
const roomId = "!1234567890:domain";
|
|
||||||
const userId = client.getUserId()!;
|
|
||||||
if (info.isDm) {
|
|
||||||
client.getAccountData = (eventType) => {
|
|
||||||
expect(eventType).toEqual("m.direct");
|
|
||||||
return mkDirectEvent(roomId, userId, info.userIds);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
DMRoomMap.makeShared(client).start();
|
|
||||||
|
|
||||||
const room = new Room(roomId, client, userId, {
|
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
||||||
});
|
|
||||||
|
|
||||||
const otherJoinEvents: MatrixEvent[] = [];
|
|
||||||
for (const otherUserId of info.userIds) {
|
|
||||||
otherJoinEvents.push(mkJoinEvent(roomId, otherUserId));
|
|
||||||
}
|
|
||||||
|
|
||||||
room.currentState.setStateEvents([
|
|
||||||
mkCreationEvent(roomId, userId),
|
|
||||||
mkNameEvent(roomId, userId, info.name),
|
|
||||||
mkJoinEvent(roomId, userId),
|
|
||||||
...otherJoinEvents,
|
|
||||||
]);
|
|
||||||
room.recalculate();
|
|
||||||
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): RenderResult {
|
|
||||||
const props: RoomHeaderProps = {
|
|
||||||
room,
|
|
||||||
inRoom: true,
|
|
||||||
onSearchClick: () => {},
|
|
||||||
onInviteClick: null,
|
|
||||||
onForgetClick: () => {},
|
|
||||||
onAppsClick: () => {},
|
|
||||||
e2eStatus: E2EStatus.Normal,
|
|
||||||
appsShown: true,
|
|
||||||
searchInfo: {
|
|
||||||
searchId: Math.random(),
|
|
||||||
promise: new Promise<ISearchResults>(() => {}),
|
|
||||||
term: "",
|
|
||||||
scope: SearchScope.Room,
|
|
||||||
count: 0,
|
|
||||||
},
|
|
||||||
viewingCall: false,
|
|
||||||
activeCall: null,
|
|
||||||
...propsOverride,
|
|
||||||
};
|
|
||||||
|
|
||||||
return render(
|
|
||||||
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
|
||||||
<RoomHeader {...props} />
|
|
||||||
</RoomContext.Provider>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkCreationEvent(roomId: string, userId: string): MatrixEvent {
|
|
||||||
return mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: "m.room.create",
|
|
||||||
room: roomId,
|
|
||||||
user: userId,
|
|
||||||
content: {
|
|
||||||
creator: userId,
|
|
||||||
room_version: "5",
|
|
||||||
predecessor: {
|
|
||||||
room_id: "!prevroom",
|
|
||||||
event_id: "$someevent",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkNameEvent(roomId: string, userId: string, name: string): MatrixEvent {
|
|
||||||
return mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: "m.room.name",
|
|
||||||
room: roomId,
|
|
||||||
user: userId,
|
|
||||||
content: { name },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkJoinEvent(roomId: string, userId: string) {
|
|
||||||
const ret = mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: "m.room.member",
|
|
||||||
room: roomId,
|
|
||||||
user: userId,
|
|
||||||
content: {
|
|
||||||
membership: "join",
|
|
||||||
avatar_url: "mxc://example.org/" + userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
ret.event.state_key = userId;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mkDirectEvent(roomId: string, userId: string, otherUsers: string[]): MatrixEvent {
|
|
||||||
const content: Record<string, string[]> = {};
|
|
||||||
for (const otherUserId of otherUsers) {
|
|
||||||
content[otherUserId] = [roomId];
|
|
||||||
}
|
|
||||||
return mkEvent({
|
|
||||||
event: true,
|
|
||||||
type: "m.direct",
|
|
||||||
room: roomId,
|
|
||||||
user: userId,
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Roomeader renders with no props 1`] = `
|
||||||
|
<DocumentFragment>
|
||||||
|
<header
|
||||||
|
class="mx_LegacyRoomHeader light-panel"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_LegacyRoomHeader_wrapper"
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
</DocumentFragment>
|
||||||
|
`;
|
|
@ -21,23 +21,23 @@ exports[`HTMLExport should export 1`] = `
|
||||||
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
|
<div class="mx_MatrixChat_wrapper" aria-hidden="false">
|
||||||
<div class="mx_MatrixChat">
|
<div class="mx_MatrixChat">
|
||||||
<main class="mx_RoomView">
|
<main class="mx_RoomView">
|
||||||
<div class="mx_RoomHeader light-panel">
|
<div class="mx_LegacyRoomHeader light-panel">
|
||||||
<div class="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
<div class="mx_LegacyRoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||||
<div class="mx_RoomHeader_avatar">
|
<div class="mx_LegacyRoomHeader_avatar">
|
||||||
<div class="mx_DecoratedRoomAvatar">
|
<div class="mx_DecoratedRoomAvatar">
|
||||||
<span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size:20.8px;width:32px;line-height:32px">!</span><img loading="lazy" class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="!myroom:example.org" style="width:32px;height:32px" aria-hidden="true" data-testid="avatar-img"/></span>
|
<span class="mx_BaseAvatar" role="presentation"><span class="mx_BaseAvatar_initial" aria-hidden="true" style="font-size:20.8px;width:32px;line-height:32px">!</span><img loading="lazy" class="mx_BaseAvatar_image" src="data:image/png;base64,00" alt="" title="!myroom:example.org" style="width:32px;height:32px" aria-hidden="true" data-testid="avatar-img"/></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx_RoomHeader_name">
|
<div class="mx_LegacyRoomHeader_name">
|
||||||
<div
|
<div
|
||||||
dir="auto"
|
dir="auto"
|
||||||
class="mx_RoomHeader_nametext"
|
class="mx_LegacyRoomHeader_nametext"
|
||||||
title="!myroom:example.org"
|
title="!myroom:example.org"
|
||||||
>
|
>
|
||||||
!myroom:example.org
|
!myroom:example.org
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mx_RoomHeader_topic" dir="auto"> </div>
|
<div class="mx_LegacyRoomHeader_topic" dir="auto"> </div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue