Merge remote-tracking branch 'origin/develop' into feat/matrix-wysisyg-integration
|
@ -14,9 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||
import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
|
@ -60,7 +61,6 @@ const testMessages = function(this: CryptoTestContext) {
|
|||
// check the invite message
|
||||
cy.contains(".mx_EventTile_body", "Hey!").closest(".mx_EventTile").within(() => {
|
||||
cy.get(".mx_EventTile_e2eIcon_warning").should("not.exist");
|
||||
cy.get(".mx_EventTile_receiptSent").should("exist");
|
||||
});
|
||||
|
||||
// Bob sends a response
|
||||
|
@ -73,17 +73,31 @@ const testMessages = function(this: CryptoTestContext) {
|
|||
};
|
||||
|
||||
const bobJoin = function(this: CryptoTestContext) {
|
||||
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
|
||||
cy.window({ log: false }).then(async win => {
|
||||
const bobRooms = this.bob.getRooms();
|
||||
if (!bobRooms.length) {
|
||||
await new Promise<void>(resolve => {
|
||||
const onMembership = (_event) => {
|
||||
this.bob.off(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
resolve();
|
||||
};
|
||||
this.bob.on(win.matrixcs.RoomMemberEvent.Membership, onMembership);
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
cy.botJoinRoomByName(this.bob, "Alice").as("bobsRoom");
|
||||
});
|
||||
|
||||
cy.contains(".mx_TextualEvent", "Bob joined the room").should("exist");
|
||||
};
|
||||
|
||||
const handleVerificationRequest = (request: VerificationRequest): Chainable<EmojiMapping[]> => {
|
||||
return cy.wrap(new Promise<EmojiMapping[]>((resolve) => {
|
||||
const onShowSas = (event: ISasEvent) => {
|
||||
resolve(event.sas.emoji);
|
||||
verifier.off("show_sas", onShowSas);
|
||||
event.confirm();
|
||||
verifier.done();
|
||||
resolve(event.sas.emoji);
|
||||
};
|
||||
|
||||
const verifier = request.beginKeyVerification("m.sas.v1");
|
||||
|
|
|
@ -109,7 +109,7 @@ describe("Lazy Loading", () => {
|
|||
}
|
||||
|
||||
function openMemberlist(): void {
|
||||
cy.get('.mx_HeaderButtons [aria-label="Room Info"]').click();
|
||||
cy.get('.mx_HeaderButtons [aria-label="Room info"]').click();
|
||||
cy.get(".mx_RoomSummaryCard").within(() => {
|
||||
cy.get(".mx_RoomSummaryCard_icon_people").click();
|
||||
});
|
||||
|
|
|
@ -58,7 +58,7 @@ describe("Login", () => {
|
|||
cy.startMeasuring("from-submit-to-home");
|
||||
cy.get(".mx_Login_submit").click();
|
||||
|
||||
cy.url().should('contain', '/#/home');
|
||||
cy.url().should('contain', '/#/home', { timeout: 30000 });
|
||||
cy.stopMeasuring("from-submit-to-home");
|
||||
});
|
||||
});
|
||||
|
|
|
@ -105,6 +105,8 @@ describe("Polls", () => {
|
|||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
// wait until Bob joined
|
||||
cy.contains(".mx_TextualEvent", "BotBob joined the room").should("exist");
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
|
@ -173,6 +175,8 @@ describe("Polls", () => {
|
|||
cy.inviteUser(roomId, botBob.getUserId());
|
||||
cy.inviteUser(roomId, botCharlie.getUserId());
|
||||
cy.visit('/#/room/' + roomId);
|
||||
// wait until the bots joined
|
||||
cy.contains(".mx_TextualEvent", "and one other were invited and joined").should("exist");
|
||||
});
|
||||
|
||||
cy.openMessageComposerOptions().within(() => {
|
||||
|
|
|
@ -72,7 +72,7 @@ describe("Registration", () => {
|
|||
cy.startMeasuring("from-submit-to-home");
|
||||
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
|
||||
|
||||
cy.get(".mx_UseCaseSelection_skip").should("exist");
|
||||
cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist");
|
||||
cy.percySnapshot("Use-case selection screen");
|
||||
cy.checkA11y();
|
||||
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
|
||||
|
|
|
@ -162,7 +162,7 @@ describe("Spotlight", () => {
|
|||
cy.window({ log: false }).then(({ matrixcs: { Visibility } }) => {
|
||||
cy.createRoom({ name: room1Name, visibility: Visibility.Public }).then(_room1Id => {
|
||||
room1Id = _room1Id;
|
||||
cy.inviteUser(room1Id, bot1.getUserId());
|
||||
bot1.joinRoom(room1Id);
|
||||
cy.visit("/#/room/" + room1Id);
|
||||
});
|
||||
bot2.createRoom({ name: room2Name, visibility: Visibility.Public })
|
||||
|
|
|
@ -73,7 +73,10 @@ describe("Threads", () => {
|
|||
|
||||
it("should be usable for a conversation", () => {
|
||||
let bot: MatrixClient;
|
||||
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||
cy.getBot(synapse, {
|
||||
displayName: "BotBob",
|
||||
autoAcceptInvites: false,
|
||||
}).then(_bot => {
|
||||
bot = _bot;
|
||||
});
|
||||
|
||||
|
@ -81,6 +84,7 @@ describe("Threads", () => {
|
|||
cy.createRoom({}).then(_roomId => {
|
||||
roomId = _roomId;
|
||||
cy.inviteUser(roomId, bot.getUserId());
|
||||
bot.joinRoom(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
Copyright 2022 Oliver Sand
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
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 { IWidget } from "matrix-widget-api";
|
||||
|
||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||
|
||||
const ROOM_NAME = 'Test Room';
|
||||
const WIDGET_ID = "fake-widget";
|
||||
const WIDGET_HTML = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Fake Widget</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello World
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
describe('Widget Layout', () => {
|
||||
let widgetUrl: string;
|
||||
let synapse: SynapseInstance;
|
||||
let roomId: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startSynapse("default").then(data => {
|
||||
synapse = data;
|
||||
|
||||
cy.initTestUser(synapse, "Sally");
|
||||
});
|
||||
cy.serveHtmlFile(WIDGET_HTML).then(url => {
|
||||
widgetUrl = url;
|
||||
});
|
||||
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME,
|
||||
}).then((id) => {
|
||||
roomId = id;
|
||||
|
||||
// setup widget via state event
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content: IWidget = {
|
||||
id: WIDGET_ID,
|
||||
creatorUserId: 'somebody',
|
||||
type: 'widget',
|
||||
name: 'widget',
|
||||
url: widgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'im.vector.modular.widgets', content, WIDGET_ID);
|
||||
}).as('widgetEventSent');
|
||||
|
||||
// set initial layout
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
|
||||
}).as('layoutEventSent');
|
||||
});
|
||||
|
||||
cy.all([
|
||||
cy.get<string>("@widgetEventSent"),
|
||||
cy.get<string>("@layoutEventSent"),
|
||||
]).then(() => {
|
||||
// open the room
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopSynapse(synapse);
|
||||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it('manually resize the height of the top container layout', () => {
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250);
|
||||
|
||||
cy.get('.mx_AppsContainer_resizerHandle')
|
||||
.trigger('mousedown')
|
||||
.trigger('mousemove', { clientX: 0, clientY: 550, force: true })
|
||||
.trigger('mouseup', { clientX: 0, clientY: 550, force: true });
|
||||
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
|
||||
});
|
||||
|
||||
it('programatically resize the height of the top container layout', () => {
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.lessThan', 250);
|
||||
|
||||
cy.getClient().then(async matrixClient => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[WIDGET_ID]: {
|
||||
container: 'top', index: 1, width: 100, height: 100,
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, 'io.element.widgets.layout', content, "");
|
||||
});
|
||||
|
||||
cy.get('iframe[title="widget"]').invoke('height').should('be.greaterThan', 400);
|
||||
});
|
||||
});
|
|
@ -100,7 +100,9 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
|||
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||
|
||||
const synapseId = await dockerRun({
|
||||
image: "matrixdotorg/synapse:develop",
|
||||
// XXX: switch back to `develop` tag once the threads receipts issue is fixed
|
||||
// https://github.com/vector-im/element-web/issues/23451
|
||||
image: "matrixdotorg/synapse:latest",
|
||||
containerName: `react-sdk-cypress-synapse`,
|
||||
params: [
|
||||
"--rm",
|
||||
|
|
|
@ -128,7 +128,7 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
|
|||
return cy.botJoinRoom(cli, room.roomId);
|
||||
}
|
||||
|
||||
return cy.wrap(Promise.reject());
|
||||
return cy.wrap(Promise.reject(`Bot room join failed. Cannot find room '${roomName}'`));
|
||||
});
|
||||
|
||||
Cypress.Commands.add("botSendMessage", (
|
||||
|
|
|
@ -97,7 +97,7 @@ Cypress.Commands.add("initTestUser", (synapse: SynapseInstance, displayName: str
|
|||
|
||||
return cy.visit("/").then(() => {
|
||||
// wait for the app to load
|
||||
return cy.get(".mx_MatrixChat", { timeout: 15000 });
|
||||
return cy.get(".mx_MatrixChat", { timeout: 30000 });
|
||||
}).then(() => ({
|
||||
password,
|
||||
accessToken: response.body.access_token,
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
"@sentry/browser": "^6.11.0",
|
||||
"@sentry/tracing": "^6.11.0",
|
||||
"@types/geojson": "^7946.0.8",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"await-lock": "^2.1.0",
|
||||
"blurhash": "^1.1.3",
|
||||
"browser-request": "^0.3.3",
|
||||
|
@ -113,6 +114,7 @@
|
|||
"rfc4648": "^1.4.0",
|
||||
"sanitize-html": "^2.3.2",
|
||||
"tar-js": "^0.3.0",
|
||||
"ua-parser-js": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
"what-input": "^5.2.10",
|
||||
"zxcvbn": "^4.4.2"
|
||||
|
|
|
@ -453,7 +453,7 @@ legend {
|
|||
}
|
||||
|
||||
@define-mixin customisedCancelButton {
|
||||
mask: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask: url('$(res)/img/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: cover;
|
||||
|
@ -466,8 +466,8 @@ legend {
|
|||
|
||||
.mx_Dialog_cancelButton {
|
||||
@mixin customisedCancelButton;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
@import "./components/views/settings/devices/_DeviceExpandDetailsButton.pcss";
|
||||
@import "./components/views/settings/devices/_DeviceSecurityCard.pcss";
|
||||
@import "./components/views/settings/devices/_DeviceTile.pcss";
|
||||
@import "./components/views/settings/devices/_DeviceType.pcss";
|
||||
@import "./components/views/settings/devices/_DeviceTypeIcon.pcss";
|
||||
@import "./components/views/settings/devices/_FilteredDeviceList.pcss";
|
||||
@import "./components/views/settings/devices/_FilteredDeviceListHeader.pcss";
|
||||
@import "./components/views/settings/devices/_SecurityRecommendations.pcss";
|
||||
|
@ -338,6 +338,7 @@
|
|||
@import "./views/spaces/_SpacePublicShare.pcss";
|
||||
@import "./views/terms/_InlineTermsAgreement.pcss";
|
||||
@import "./views/toasts/_AnalyticsToast.pcss";
|
||||
@import "./views/toasts/_IncomingCallToast.pcss";
|
||||
@import "./views/toasts/_IncomingLegacyCallToast.pcss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.pcss";
|
||||
@import "./views/typography/_Heading.pcss";
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_DeviceType {
|
||||
.mx_DeviceTypeIcon {
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
margin-right: $spacing-8;
|
||||
|
@ -22,7 +22,7 @@ limitations under the License.
|
|||
padding: 0 $spacing-8 $spacing-8 0;
|
||||
}
|
||||
|
||||
.mx_DeviceType_deviceIcon {
|
||||
.mx_DeviceTypeIcon_deviceIconWrapper {
|
||||
--background-color: $system;
|
||||
--icon-color: $secondary-content;
|
||||
|
||||
|
@ -36,12 +36,17 @@ limitations under the License.
|
|||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.mx_DeviceType_selected .mx_DeviceType_deviceIcon {
|
||||
.mx_DeviceTypeIcon_selected .mx_DeviceTypeIcon_deviceIconWrapper {
|
||||
--background-color: $primary-content;
|
||||
--icon-color: $background;
|
||||
}
|
||||
|
||||
.mx_DeviceType_verificationIcon {
|
||||
.mx_DeviceTypeIcon_deviceIcon {
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
.mx_DeviceTypeIcon_verificationIcon {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
|
@ -17,20 +17,3 @@ limitations under the License.
|
|||
.mx_HeaderButtons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_buttons + .mx_HeaderButtons {
|
||||
/* remove the | separator line for when next to RoomHeaderButtons */
|
||||
/* TODO: remove this once when we redo communities and make the right panel similar to the new rooms one */
|
||||
&::before {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_HeaderButtons::before {
|
||||
content: "";
|
||||
background-color: $header-panel-text-primary-color;
|
||||
opacity: 0.5;
|
||||
margin: 6px 8px;
|
||||
border-radius: 1px;
|
||||
width: 1px;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,10 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_voiceIcon::before {
|
||||
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSettingsDialog_securityIcon::before {
|
||||
mask-image: url('$(res)/img/element-icons/security.svg');
|
||||
}
|
||||
|
|
|
@ -76,11 +76,6 @@ limitations under the License.
|
|||
border: 0;
|
||||
text-align: center;
|
||||
|
||||
&:not(.mx_Tooltip_noMargin) {
|
||||
margin-left: 6px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.mx_Tooltip_chevron {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -68,8 +68,10 @@ limitations under the License.
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg');
|
||||
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-size: 20px;
|
||||
mask-position: center 6px;
|
||||
transform: rotate(180deg);
|
||||
background: $muted-fg-color;
|
||||
}
|
||||
|
|
|
@ -19,16 +19,27 @@ limitations under the License.
|
|||
border-bottom: 1px solid $primary-hairline-color;
|
||||
background-color: $background;
|
||||
|
||||
.mx_RoomHeader_e2eIcon {
|
||||
.mx_RoomHeader_icon {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
|
||||
.mx_E2EIcon {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,7 +49,7 @@ limitations under the License.
|
|||
align-items: center;
|
||||
min-width: 0;
|
||||
margin: 0 20px 0 16px;
|
||||
padding-top: 8px;
|
||||
padding-top: 6px;
|
||||
border-bottom: 1px solid $system;
|
||||
|
||||
.mx_InviteOnlyIcon_large {
|
||||
|
@ -77,11 +88,6 @@ limitations under the License.
|
|||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_buttons {
|
||||
display: flex;
|
||||
background-color: $background;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_info {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
|
@ -93,9 +99,11 @@ limitations under the License.
|
|||
overflow: hidden;
|
||||
color: $primary-content;
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-18px;
|
||||
font-size: $font-15px;
|
||||
min-height: 24px;
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
margin: 0 7px;
|
||||
margin: 0 3px;
|
||||
padding: 1px 4px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
|
@ -112,10 +120,10 @@ limitations under the License.
|
|||
|
||||
.mx_RoomHeader_chevron {
|
||||
align-self: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-size: 20px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
background-color: $tertiary-content;
|
||||
|
@ -160,9 +168,6 @@ limitations under the License.
|
|||
line-height: $lineHeight;
|
||||
max-height: calc($lineHeight * $lines);
|
||||
|
||||
/* to align baseline of topic with room name */
|
||||
margin: 4px 7px 0;
|
||||
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: $lines; /* See: https://drafts.csswg.org/css-overflow-3/#webkit-line-clamp */
|
||||
-webkit-box-orient: vertical;
|
||||
|
@ -177,7 +182,7 @@ limitations under the License.
|
|||
|
||||
.mx_RoomHeader_avatar {
|
||||
flex: 0;
|
||||
margin: 0 6px 0 7px;
|
||||
margin: 0 7px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -206,7 +211,7 @@ limitations under the License.
|
|||
mask-size: contain;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:not(.mx_RoomHeader_closeButton):hover {
|
||||
background: rgba($accent, 0.1);
|
||||
|
||||
&::before {
|
||||
|
@ -249,6 +254,37 @@ limitations under the License.
|
|||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
|
@ -51,11 +51,11 @@ limitations under the License.
|
|||
position: absolute;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down-thin.svg');
|
||||
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-size: 20px;
|
||||
mask-position: center;
|
||||
background: $muted-fg-color;
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.mx_TopUnreadMessagesBar_markAsRead {
|
||||
|
|
|
@ -58,7 +58,7 @@ limitations under the License.
|
|||
min-height: 35px;
|
||||
padding: 0 $spacing-8;
|
||||
|
||||
.mx_DeviceType {
|
||||
.mx_DeviceTypeIcon {
|
||||
/* hide the new device type in legacy device list
|
||||
for backwards compat reasons */
|
||||
display: none;
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_IncomingCallToast {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
pointer-events: initial; /* restore pointer events so the user can accept/decline */
|
||||
width: 250px;
|
||||
|
||||
.mx_IncomingCallToast_content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 8px;
|
||||
width: 100%;
|
||||
|
||||
.mx_IncomingCallToast_info {
|
||||
margin-bottom: $spacing-16;
|
||||
|
||||
.mx_IncomingCallToast_room {
|
||||
display: inline-block;
|
||||
|
||||
font-weight: bold;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
|
||||
margin-bottom: $spacing-4;
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_message {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
|
||||
margin-bottom: $spacing-4;
|
||||
}
|
||||
|
||||
.mx_LiveContentSummary {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-15px;
|
||||
|
||||
.mx_LiveContentSummary_participants::before {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_joinButton {
|
||||
position: relative;
|
||||
|
||||
bottom: $spacing-4;
|
||||
right: $spacing-4;
|
||||
|
||||
align-self: flex-end;
|
||||
|
||||
box-sizing: border-box;
|
||||
min-width: 120px;
|
||||
|
||||
padding: $spacing-4 0;
|
||||
|
||||
line-height: $font-24px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IncomingCallToast_closeButton {
|
||||
position: absolute;
|
||||
|
||||
top: $spacing-4;
|
||||
right: $spacing-4;
|
||||
|
||||
display: flex;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
||||
mask-image: url('$(res)/img/cancel.svg');
|
||||
|
||||
height: inherit;
|
||||
width: inherit;
|
||||
background-color: $secondary-content;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ limitations under the License.
|
|||
width: 100%;
|
||||
|
||||
&.mx_LegacyCallViewHeader_pip {
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 1</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<path d="M9.74464309,-3.02908503 L8.14106175,-3.02908503 L8.14106175,8.19448443 L-3.03028759,8.19448443 L-3.03028759,9.7978515 L8.14106175,9.7978515 L8.14106175,20.9685098 L9.74464309,20.9685098 L9.74464309,9.7978515 L20.9697124,9.7978515 L20.9697124,8.19448443 L9.74464309,8.19448443 L9.74464309,-3.02908503" id="Fill-108" opacity="0.9" fill="#454545" sketch:type="MSShapeGroup" transform="translate(8.969712, 8.969712) rotate(-315.000000) translate(-8.969712, -8.969712) "></path>
|
||||
</g>
|
||||
</svg>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.2756 4.69628C21.8922 4.07969 21.8922 3.08 21.2756 2.46342C20.659 1.84683 19.6593 1.84683 19.0427 2.46342L11.8917 9.61447L4.74063 2.46342C4.12404 1.84683 3.12436 1.84683 2.50777 2.46342C1.89118 3.08 1.89118 4.07969 2.50777 4.69628L9.65882 11.8473L2.20145 19.3047C1.58487 19.9213 1.58487 20.921 2.20145 21.5376C2.81804 22.1541 3.81773 22.1541 4.43431 21.5376L11.8917 14.0802L19.349 21.5375C19.9656 22.1541 20.9653 22.1541 21.5819 21.5375C22.1985 20.921 22.1985 19.9213 21.5819 19.3047L14.1245 11.8473L21.2756 4.69628Z" fill="#737D8C"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 650 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.28571 3.66667C6.28571 2.74619 7.03191 2 7.95238 2H11.7619C12.6824 2 13.4286 2.74619 13.4286 3.66667V7.47619C13.4286 8.39666 12.6824 9.14286 11.7619 9.14286H7.95238C7.03191 9.14286 6.28571 8.39667 6.28571 7.47619V3.66667ZM16.5238 2C15.6033 2 14.8571 2.74619 14.8571 3.66667V7.47619C14.8571 8.39667 15.6033 9.14286 16.5238 9.14286H20.3333C21.2538 9.14286 22 8.39666 22 7.47619V3.66667C22 2.74619 21.2538 2 20.3333 2H16.5238ZM16.5238 10.5714C15.6033 10.5714 14.8571 11.3176 14.8571 12.2381V16.0476C14.8571 16.9681 15.6033 17.7143 16.5238 17.7143H20.3333C21.2538 17.7143 22 16.9681 22 16.0476V12.2381C22 11.3176 21.2538 10.5714 20.3333 10.5714H16.5238ZM3.63265 10.5714C2.73096 10.5714 2 11.3024 2 12.2041V20.3673C2 21.269 2.73097 22 3.63266 22H11.7959C12.6976 22 13.4286 21.269 13.4286 20.3673V12.2041C13.4286 11.3024 12.6976 10.5714 11.7959 10.5714H3.63265Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M4 17.8689V2.51551C4 2.06669 4.53728 1.83452 4.86986 2.13591C8.18767 5.14263 10.9111 7.48209 13.102 9.36399L13.102 9.36403C18.3295 13.8544 20.5243 15.7398 20.5243 17.8689C20.5243 19.4181 19.6538 20.0153 18.1044 20.79C16.5549 21.5648 14.4534 22 12.2621 22C10.0709 22 7.96938 21.5648 6.41992 20.79C4.87047 20.0153 4 18.9646 4 17.8689ZM12.2621 20.9673C16.2548 20.9673 19.4915 19.5801 19.4915 17.869C19.4915 16.1578 16.2548 14.7707 12.2621 14.7707C8.26947 14.7707 5.03277 16.1578 5.03277 17.869C5.03277 19.5801 8.26947 20.9673 12.2621 20.9673ZM16.2618 8.67876C16.1718 8.64549 16.1718 8.51831 16.2618 8.48504L17.84 7.90103C17.8683 7.89057 17.8906 7.86828 17.901 7.84001L18.4851 6.26174C18.5183 6.17182 18.6455 6.17182 18.6788 6.26174L19.2628 7.84001C19.2733 7.86828 19.2955 7.89057 19.3238 7.90103L20.9021 8.48504C20.992 8.51831 20.992 8.64549 20.9021 8.67876L19.3238 9.26277C19.2955 9.27323 19.2733 9.29552 19.2628 9.32379L18.6788 10.9021C18.6455 10.992 18.5183 10.992 18.4851 10.9021L17.901 9.32379C17.8906 9.29552 17.8683 9.27323 17.84 9.26277L16.2618 8.67876ZM13.2618 5.45232C13.1718 5.48559 13.1718 5.61276 13.2618 5.64604L14.0862 5.95111C14.1145 5.96157 14.1368 5.98386 14.1472 6.01213L14.4523 6.83657C14.4856 6.92649 14.6127 6.92649 14.646 6.83657L14.9511 6.01213C14.9615 5.98386 14.9838 5.96157 15.0121 5.95111L15.8365 5.64603C15.9265 5.61276 15.9265 5.48559 15.8365 5.45232L15.0121 5.14725C14.9838 5.13679 14.9615 5.1145 14.9511 5.08623L14.646 4.26178C14.6127 4.17187 14.4856 4.17187 14.4523 4.26178L14.1472 5.08623C14.1368 5.1145 14.1145 5.13679 14.0862 5.14725L13.2618 5.45232Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,4 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.0759 13.6172C13.0273 13.7343 13.0004 13.8625 13 13.997C13 13.998 13 13.999 13 14L13 14.0007L13 20C13 20.5523 13.4477 21 14 21C14.5523 21 15 20.5523 15 20L15 16.4142L18.7929 20.2071C19.1834 20.5976 19.8166 20.5976 20.2071 20.2071C20.5976 19.8166 20.5976 19.1834 20.2071 18.7929L16.4142 15L20 15C20.5523 15 21 14.5523 21 14C21 13.4477 20.5523 13 20 13L14.0007 13L14 13C13.999 13 13.998 13 13.997 13C13.743 13.0008 13.4892 13.0977 13.295 13.2908C13.2943 13.2915 13.2936 13.2922 13.2929 13.2929C13.2922 13.2936 13.2915 13.2943 13.2908 13.295C13.196 13.3904 13.1243 13.5001 13.0759 13.6172ZM13.0759 13.6172C13.1262 13.4959 13.1996 13.3867 13.2908 13.295L13.0759 13.6172Z" fill="#737D8C"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9241 10.3828C10.9727 10.2657 10.9996 10.1375 11 10.003C11 10.002 11 10.001 11 10V9.9993L11 4C11 3.44772 10.5523 3 10 3C9.44772 3 9 3.44772 9 4L9 7.58579L5.20711 3.79289C4.81658 3.40237 4.18342 3.40237 3.79289 3.79289C3.40237 4.18342 3.40237 4.81658 3.79289 5.20711L7.58579 9L4 9C3.44771 9 3 9.44771 3 10C3 10.5523 3.44771 11 4 11L9.9993 11H10C10.001 11 10.002 11 10.003 11C10.257 10.9992 10.5108 10.9023 10.705 10.7092C10.7057 10.7085 10.7064 10.7078 10.7071 10.7071C10.7078 10.7064 10.7085 10.7057 10.7092 10.705C10.804 10.6096 10.8757 10.4999 10.9241 10.3828ZM10.9241 10.3828C10.8738 10.5041 10.8004 10.6133 10.7092 10.705L10.9241 10.3828Z" fill="#737D8C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,3 @@
|
|||
<svg width="22" height="19" viewBox="0 0 22 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 15.5C2.45 15.5 1.97933 15.3043 1.588 14.913C1.196 14.521 1 14.05 1 13.5V2.5C1 1.95 1.196 1.479 1.588 1.087C1.97933 0.695667 2.45 0.5 3 0.5H19C19.55 0.5 20.021 0.695667 20.413 1.087C20.8043 1.479 21 1.95 21 2.5V13.5C21 14.05 20.8043 14.521 20.413 14.913C20.021 15.3043 19.55 15.5 19 15.5H3ZM3 13.5H19V2.5H3V13.5ZM1 18.5C0.716667 18.5 0.479333 18.404 0.288 18.212C0.096 18.0207 0 17.7833 0 17.5C0 17.2167 0.096 16.9793 0.288 16.788C0.479333 16.596 0.716667 16.5 1 16.5H21C21.2833 16.5 21.5207 16.596 21.712 16.788C21.904 16.9793 22 17.2167 22 17.5C22 17.7833 21.904 18.0207 21.712 18.212C21.5207 18.404 21.2833 18.5 21 18.5H1ZM3 13.5V2.5V13.5Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 780 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="23" viewBox="0 0 14 23" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 0.51L2 0.5C0.9 0.5 0 1.4 0 2.5V20.5C0 21.6 0.9 22.5 2 22.5H12C13.1 22.5 14 21.6 14 20.5V2.5C14 1.4 13.1 0.51 12 0.51ZM12 18.5H2V4.5H12V18.5Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 280 B |
|
@ -0,0 +1,3 @@
|
|||
<svg width="20" height="17" viewBox="0 0 20 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 16.5H2C1.45 16.5 0.979333 16.3043 0.588 15.913C0.196 15.521 0 15.05 0 14.5V2.5C0 1.95 0.196 1.47933 0.588 1.088C0.979333 0.696 1.45 0.5 2 0.5H18C18.55 0.5 19.021 0.696 19.413 1.088C19.8043 1.47933 20 1.95 20 2.5V14.5C20 15.05 19.8043 15.521 19.413 15.913C19.021 16.3043 18.55 16.5 18 16.5ZM2 4.5V14.5H18V4.5H2Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 450 B |
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
|
||||
<!-- Generator: Sketch 3.4.2 (15857) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>Slice 1</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="#" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
|
||||
<path d="M9.74464309,-3.02908503 L8.14106175,-3.02908503 L8.14106175,8.19448443 L-3.03028759,8.19448443 L-3.03028759,9.7978515 L8.14106175,9.7978515 L8.14106175,20.9685098 L9.74464309,20.9685098 L9.74464309,9.7978515 L20.9697124,9.7978515 L20.9697124,8.19448443 L9.74464309,8.19448443 L9.74464309,-3.02908503" id="Fill-108" opacity="0.9" fill="#9fa9ba" sketch:type="MSShapeGroup" transform="translate(8.969712, 8.969712) rotate(-315.000000) translate(-8.969712, -8.969712) "></path>
|
||||
</g>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.0 KiB |
|
@ -1,3 +0,0 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 7.5L9 10.5L12 7.5" stroke="black" stroke-width="0.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 217 B |
|
@ -49,3 +49,8 @@ export type KeysWithObjectShape<Input> = {
|
|||
? (Input[P] extends Array<unknown> ? never : P)
|
||||
: never;
|
||||
}[keyof Input];
|
||||
|
||||
export type KeysStartingWith<Input extends object, Str extends string> = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
[P in keyof Input]: P extends `${Str}${infer _X}` ? P : never; // we don't use _X
|
||||
}[keyof Input];
|
||||
|
|
|
@ -117,8 +117,10 @@ export interface IConfigOptions {
|
|||
obey_asserted_identity?: boolean; // MSC3086
|
||||
};
|
||||
element_call: {
|
||||
url: string;
|
||||
use_exclusively: boolean;
|
||||
url?: string;
|
||||
use_exclusively?: boolean;
|
||||
participant_limit?: number;
|
||||
brand?: string;
|
||||
};
|
||||
|
||||
logout_redirect_url?: string;
|
||||
|
@ -179,9 +181,6 @@ export interface IConfigOptions {
|
|||
|
||||
sync_timeline_limit?: number;
|
||||
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
|
||||
|
||||
// XXX: Undocumented URL for the "Learn more about spaces" link in the "Communities don't exist" messaging.
|
||||
spaces_learn_more_url?: string;
|
||||
}
|
||||
|
||||
export interface ISsoRedirectOptions {
|
||||
|
|
|
@ -26,6 +26,7 @@ import { M_LOCATION } from "matrix-js-sdk/src/@types/location";
|
|||
import {
|
||||
PermissionChanged as PermissionChangedEvent,
|
||||
} from "@matrix-org/analytics-events/types/typescript/PermissionChanged";
|
||||
import { ISyncStateData, SyncState } from "matrix-js-sdk/src/sync";
|
||||
|
||||
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||
import { PosthogAnalytics } from "./PosthogAnalytics";
|
||||
|
@ -47,6 +48,10 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
|||
import LegacyCallHandler from "./LegacyCallHandler";
|
||||
import VoipUserMapper from "./VoipUserMapper";
|
||||
import { localNotificationsAreSilenced } from "./utils/notifications";
|
||||
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
import { ElementCall } from "./models/Call";
|
||||
import { createLocalNotificationSettingsIfNeeded } from './utils/notifications';
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -348,17 +353,25 @@ export const Notifier = {
|
|||
return this.toolbarHidden;
|
||||
},
|
||||
|
||||
onSyncStateChange: function(state: string) {
|
||||
if (state === "SYNCING") {
|
||||
onSyncStateChange: function(state: SyncState, prevState?: SyncState, data?: ISyncStateData) {
|
||||
if (state === SyncState.Syncing) {
|
||||
this.isSyncing = true;
|
||||
} else if (state === "STOPPED" || state === "ERROR") {
|
||||
} else if (state === SyncState.Stopped || state === SyncState.Error) {
|
||||
this.isSyncing = false;
|
||||
}
|
||||
|
||||
// wait for first non-cached sync to complete
|
||||
if (
|
||||
![SyncState.Stopped, SyncState.Error].includes(state) &&
|
||||
!data?.fromCache
|
||||
) {
|
||||
createLocalNotificationSettingsIfNeeded(MatrixClientPeg.get());
|
||||
}
|
||||
},
|
||||
|
||||
onEvent: function(ev: MatrixEvent) {
|
||||
if (!this.isSyncing) return; // don't alert for any messages initially
|
||||
if (ev.getSender() === MatrixClientPeg.get().credentials.userId) return;
|
||||
if (ev.getSender() === MatrixClientPeg.get().getUserId()) return;
|
||||
|
||||
MatrixClientPeg.get().decryptEventIfNeeded(ev);
|
||||
|
||||
|
@ -419,6 +432,8 @@ export const Notifier = {
|
|||
|
||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
if (actions?.notify) {
|
||||
this._performCustomEventHandling(ev);
|
||||
|
||||
if (RoomViewStore.instance.getRoomId() === room.roomId &&
|
||||
UserActivity.sharedInstance().userActiveRecently() &&
|
||||
!Modal.hasDialogs()
|
||||
|
@ -436,6 +451,24 @@ export const Notifier = {
|
|||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Some events require special handling such as showing in-app toasts
|
||||
*/
|
||||
_performCustomEventHandling: function(ev: MatrixEvent) {
|
||||
if (
|
||||
ElementCall.CALL_EVENT_TYPE.names.includes(ev.getType())
|
||||
&& SettingsStore.getValue("feature_group_calls")
|
||||
) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: getIncomingCallToastKey(ev.getStateKey()),
|
||||
priority: 100,
|
||||
component: IncomingCallToast,
|
||||
bodyClassName: "mx_IncomingCallToast",
|
||||
props: { callEvent: ev },
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
if (!window.mxNotifier) {
|
||||
|
|
|
@ -20,7 +20,6 @@ enum PageType {
|
|||
HomePage = "home_page",
|
||||
RoomView = "room_view",
|
||||
UserView = "user_view",
|
||||
LegacyGroupView = "legacy_group_view",
|
||||
}
|
||||
|
||||
export default PageType;
|
||||
|
|
|
@ -41,7 +41,6 @@ const loggedInPageTypeMap: Record<PageType, ScreenName> = {
|
|||
[PageType.HomePage]: "Home",
|
||||
[PageType.RoomView]: "Room",
|
||||
[PageType.UserView]: "User",
|
||||
[PageType.LegacyGroupView]: "Group",
|
||||
};
|
||||
|
||||
export default class PosthogTrackers {
|
||||
|
|
|
@ -33,6 +33,8 @@ export const DEFAULTS: IConfigOptions = {
|
|||
element_call: {
|
||||
url: "https://call.element.io",
|
||||
use_exclusively: false,
|
||||
participant_limit: 8,
|
||||
brand: "Element Call",
|
||||
},
|
||||
|
||||
// @ts-ignore - we deliberately use the camelCase version here so we trigger
|
||||
|
@ -44,7 +46,6 @@ export const DEFAULTS: IConfigOptions = {
|
|||
logo: require("../res/img/element-desktop-logo.svg").default,
|
||||
url: "https://element.io/get-started",
|
||||
},
|
||||
spaces_learn_more_url: "https://element.io/blog/spaces-blast-out-of-beta/",
|
||||
};
|
||||
|
||||
export default class SdkConfig {
|
||||
|
|
|
@ -45,6 +45,7 @@ import AccessibleButton from './components/views/elements/AccessibleButton';
|
|||
import RightPanelStore from './stores/right-panel/RightPanelStore';
|
||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||
import { isLocationEvent } from './utils/EventUtils';
|
||||
import { ElementCall } from "./models/Call";
|
||||
|
||||
export function getSenderName(event: MatrixEvent): string {
|
||||
return event.sender?.name ?? event.getSender() ?? _t("Someone");
|
||||
|
@ -57,6 +58,15 @@ function getRoomMemberDisplayname(event: MatrixEvent, userId = event.getSender()
|
|||
return member?.name || member?.rawDisplayName || userId || _t("Someone");
|
||||
}
|
||||
|
||||
function textForCallEvent(event: MatrixEvent): () => string {
|
||||
const roomName = MatrixClientPeg.get().getRoom(event.getRoomId()!).name;
|
||||
const isSupported = MatrixClientPeg.get().supportsVoip();
|
||||
|
||||
return isSupported
|
||||
? () => _t("Video call started in %(roomName)s.", { roomName })
|
||||
: () => _t("Video call started in %(roomName)s. (not supported by this browser)", { roomName });
|
||||
}
|
||||
|
||||
// These functions are frequently used just to check whether an event has
|
||||
// any text to display at all. For this reason they return deferred values
|
||||
// to avoid the expense of looking up translations when they're not needed.
|
||||
|
@ -798,6 +808,11 @@ for (const evType of ALL_RULE_TYPES) {
|
|||
stateHandlers[evType] = textForMjolnirEvent;
|
||||
}
|
||||
|
||||
// Add both stable and unstable m.call events
|
||||
for (const evType of ElementCall.CALL_EVENT_TYPE.names) {
|
||||
stateHandlers[evType] = textForCallEvent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given event has text to display.
|
||||
* @param ev The event
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -15,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React, { createRef } from 'react';
|
||||
import React from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
|
@ -23,6 +24,8 @@ import { _t } from '../../../../languageHandler';
|
|||
import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryption';
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
import { KeysStartingWith } from "../../../../@types/common";
|
||||
|
||||
enum Phase {
|
||||
Edit = "edit",
|
||||
|
@ -36,12 +39,14 @@ interface IProps extends IDialogProps {
|
|||
interface IState {
|
||||
phase: Phase;
|
||||
errStr: string;
|
||||
passphrase1: string;
|
||||
passphrase2: string;
|
||||
}
|
||||
|
||||
type AnyPassphrase = KeysStartingWith<IState, "passphrase">;
|
||||
|
||||
export default class ExportE2eKeysDialog extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private passphrase1 = createRef<HTMLInputElement>();
|
||||
private passphrase2 = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -49,6 +54,8 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
this.state = {
|
||||
phase: Phase.Edit,
|
||||
errStr: null,
|
||||
passphrase1: "",
|
||||
passphrase2: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -59,8 +66,8 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
private onPassphraseFormSubmit = (ev: React.FormEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
|
||||
const passphrase = this.passphrase1.current.value;
|
||||
if (passphrase !== this.passphrase2.current.value) {
|
||||
const passphrase = this.state.passphrase1;
|
||||
if (passphrase !== this.state.passphrase2) {
|
||||
this.setState({ errStr: _t('Passphrases must match') });
|
||||
return false;
|
||||
}
|
||||
|
@ -112,6 +119,12 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
return false;
|
||||
};
|
||||
|
||||
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>, phrase: AnyPassphrase) => {
|
||||
this.setState({
|
||||
[phrase]: ev.target.value,
|
||||
} as Pick<IState, AnyPassphrase>);
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const disableForm = (this.state.phase === Phase.Exporting);
|
||||
|
||||
|
@ -146,36 +159,25 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputTable'>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase1'>
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this.passphrase1}
|
||||
id='passphrase1'
|
||||
autoFocus={true}
|
||||
size={64}
|
||||
type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
<Field
|
||||
label={_t("Enter passphrase")}
|
||||
value={this.state.passphrase1}
|
||||
onChange={e => this.onPassphraseChange(e, "passphrase1")}
|
||||
autoFocus={true}
|
||||
size={64}
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase2'>
|
||||
{ _t("Confirm passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input ref={this.passphrase2}
|
||||
id='passphrase2'
|
||||
size={64}
|
||||
type='password'
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
<Field
|
||||
label={_t("Confirm passphrase")}
|
||||
value={this.state.passphrase2}
|
||||
onChange={e => this.onPassphraseChange(e, "passphrase2")}
|
||||
size={64}
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
|
@ -22,6 +23,7 @@ import * as MegolmExportEncryption from '../../../../utils/MegolmExportEncryptio
|
|||
import { _t } from '../../../../languageHandler';
|
||||
import { IDialogProps } from "../../../../components/views/dialogs/IDialogProps";
|
||||
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
|
||||
import Field from "../../../../components/views/elements/Field";
|
||||
|
||||
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -48,12 +50,12 @@ interface IState {
|
|||
enableSubmit: boolean;
|
||||
phase: Phase;
|
||||
errStr: string;
|
||||
passphrase: string;
|
||||
}
|
||||
|
||||
export default class ImportE2eKeysDialog extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private file = createRef<HTMLInputElement>();
|
||||
private passphrase = createRef<HTMLInputElement>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
@ -62,6 +64,7 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
enableSubmit: false,
|
||||
phase: Phase.Edit,
|
||||
errStr: null,
|
||||
passphrase: "",
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -69,16 +72,22 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onFormChange = (ev: React.FormEvent): void => {
|
||||
private onFormChange = (): void => {
|
||||
const files = this.file.current.files || [];
|
||||
this.setState({
|
||||
enableSubmit: (this.passphrase.current.value !== "" && files.length > 0),
|
||||
enableSubmit: (this.state.passphrase !== "" && files.length > 0),
|
||||
});
|
||||
};
|
||||
|
||||
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>): void => {
|
||||
this.setState({ passphrase: ev.target.value });
|
||||
this.onFormChange(); // update general form state too
|
||||
};
|
||||
|
||||
private onFormSubmit = (ev: React.FormEvent): boolean => {
|
||||
ev.preventDefault();
|
||||
this.startImport(this.file.current.files[0], this.passphrase.current.value);
|
||||
// noinspection JSIgnoredPromiseFromCall
|
||||
this.startImport(this.file.current.files[0], this.state.passphrase);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
@ -161,20 +170,14 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
</div>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputRow'>
|
||||
<div className='mx_E2eKeysDialog_inputLabel'>
|
||||
<label htmlFor='passphrase'>
|
||||
{ _t("Enter passphrase") }
|
||||
</label>
|
||||
</div>
|
||||
<div className='mx_E2eKeysDialog_inputCell'>
|
||||
<input
|
||||
ref={this.passphrase}
|
||||
id='passphrase'
|
||||
size={64}
|
||||
type='password'
|
||||
onChange={this.onFormChange}
|
||||
disabled={disableForm} />
|
||||
</div>
|
||||
<Field
|
||||
label={_t("Enter passphrase")}
|
||||
value={this.state.passphrase}
|
||||
onChange={this.onPassphraseChange}
|
||||
size={64}
|
||||
type="password"
|
||||
disabled={disableForm}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 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 * as React from "react";
|
||||
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { _t } from "../../languageHandler";
|
||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
||||
|
||||
interface IProps {
|
||||
groupId: string;
|
||||
}
|
||||
|
||||
const LegacyGroupView: React.FC<IProps> = ({ groupId }) => {
|
||||
// XXX: Stealing classes from the HomePage component for CSS simplicity.
|
||||
// XXX: Inline CSS because this is all temporary
|
||||
const learnMoreUrl = SdkConfig.get().spaces_learn_more_url ?? DEFAULTS.spaces_learn_more_url;
|
||||
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
|
||||
<div className="mx_HomePage_default_wrapper">
|
||||
<h1 style={{ fontSize: '24px' }}>{ _t("That link is no longer supported") }</h1>
|
||||
<p>
|
||||
{ _t(
|
||||
"You're trying to access a community link (%(groupId)s).<br/>" +
|
||||
"Communities are no longer supported and have been replaced by spaces.<br2/>" +
|
||||
"<a>Learn more about spaces here.</a>",
|
||||
{ groupId },
|
||||
{
|
||||
br: () => <br />,
|
||||
br2: () => <br />,
|
||||
a: (sub) => <a href={learnMoreUrl} rel="noreferrer noopener" target="_blank">{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
</div>
|
||||
</AutoHideScrollbar>;
|
||||
};
|
||||
|
||||
export default LegacyGroupView;
|
|
@ -67,7 +67,6 @@ import RightPanelStore from '../../stores/right-panel/RightPanelStore';
|
|||
import { TimelineRenderingType } from "../../contexts/RoomContext";
|
||||
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
|
||||
import { SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
|
||||
import LegacyGroupView from "./LegacyGroupView";
|
||||
import { IConfigOptions } from "../../IConfigOptions";
|
||||
import LeftPanelLiveShareWarning from '../views/beacon/LeftPanelLiveShareWarning';
|
||||
import { UserOnboardingPage } from '../views/user-onboarding/UserOnboardingPage';
|
||||
|
@ -103,8 +102,6 @@ interface IProps {
|
|||
justRegistered?: boolean;
|
||||
roomJustCreatedOpts?: IOpts;
|
||||
forceTimeline?: boolean; // see props on MatrixChat
|
||||
|
||||
currentGroupId?: string;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -641,10 +638,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
case PageTypes.UserView:
|
||||
pageElement = <UserView userId={this.props.currentUserId} resizeNotifier={this.props.resizeNotifier} />;
|
||||
break;
|
||||
|
||||
case PageTypes.LegacyGroupView:
|
||||
pageElement = <LegacyGroupView groupId={this.props.currentGroupId} />;
|
||||
break;
|
||||
}
|
||||
|
||||
const wrapperClasses = classNames({
|
||||
|
|
|
@ -188,8 +188,6 @@ interface IState {
|
|||
currentRoomId?: string;
|
||||
// If we're trying to just view a user ID (i.e. /user URL), this is it
|
||||
currentUserId?: string;
|
||||
// Group ID for legacy "communities don't exist" page
|
||||
currentGroupId?: string;
|
||||
// this is persisted as mx_lhs_size, loaded in LoggedInView
|
||||
collapseLhs: boolean;
|
||||
// Parameters used in the registration dance with the IS
|
||||
|
@ -679,9 +677,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
break;
|
||||
}
|
||||
case 'view_legacy_group':
|
||||
this.viewLegacyGroup(payload.groupId);
|
||||
break;
|
||||
case Action.ViewUserSettings: {
|
||||
const tabPayload = payload as OpenToTabPayload;
|
||||
Modal.createDialog(UserSettingsDialog,
|
||||
|
@ -1023,16 +1018,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
private viewLegacyGroup(groupId: string) {
|
||||
this.setStateForNewView({
|
||||
view: Views.LOGGED_IN,
|
||||
currentRoomId: null,
|
||||
currentGroupId: groupId,
|
||||
});
|
||||
this.notifyNewScreen('group/' + groupId);
|
||||
this.setPage(PageType.LegacyGroupView);
|
||||
}
|
||||
|
||||
private async createRoom(defaultPublic = false, defaultName?: string, type?: RoomType) {
|
||||
const modal = Modal.createDialog(CreateRoomDialog, {
|
||||
type,
|
||||
|
@ -1803,12 +1788,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
userId: userId,
|
||||
subAction: params.action,
|
||||
});
|
||||
} else if (screen.indexOf('group/') === 0) {
|
||||
const groupId = screen.substring(6);
|
||||
dis.dispatch({
|
||||
action: 'view_legacy_group',
|
||||
groupId: groupId,
|
||||
});
|
||||
} else {
|
||||
logger.info("Ignoring showScreen for '%s'", screen);
|
||||
}
|
||||
|
|
|
@ -121,6 +121,7 @@ import { LargeLoader } from './LargeLoader';
|
|||
import { VoiceBroadcastInfoEventType } from '../../voice-broadcast';
|
||||
import { isVideoRoom } from '../../utils/video-rooms';
|
||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||
import { Call } from "../../models/Call";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -178,6 +179,7 @@ export interface IRoomState {
|
|||
searchHighlights?: string[];
|
||||
searchInProgress?: boolean;
|
||||
callState?: CallState;
|
||||
activeCall: Call | null;
|
||||
canPeek: boolean;
|
||||
canSelfRedact: boolean;
|
||||
showApps: boolean;
|
||||
|
@ -303,6 +305,8 @@ function LocalRoomView(props: LocalRoomViewProps): ReactElement {
|
|||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
<main className="mx_RoomView_body" ref={props.roomView}>
|
||||
<FileDropTarget parent={props.roomView.current} onFileDrop={props.onFileDrop} />
|
||||
|
@ -353,6 +357,8 @@ function LocalRoomCreateLoader(props: ILocalRoomCreateLoaderProps): ReactElement
|
|||
excludedRightPanelPhaseButtons={[]}
|
||||
showButtons={false}
|
||||
enableRoomOptionsMenu={false}
|
||||
viewingCall={false}
|
||||
activeCall={null}
|
||||
/>
|
||||
<div className="mx_RoomView_body">
|
||||
<LargeLoader text={text} />
|
||||
|
@ -391,6 +397,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
numUnreadMessages: 0,
|
||||
searchResults: null,
|
||||
callState: null,
|
||||
activeCall: null,
|
||||
canPeek: false,
|
||||
canSelfRedact: false,
|
||||
showApps: false,
|
||||
|
@ -497,13 +504,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
if (WidgetLayoutStore.instance.hasMaximisedWidget(this.state.room)) {
|
||||
// Show chat in right panel when a widget is maximised
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.Timeline });
|
||||
} else if (
|
||||
RightPanelStore.instance.isOpen &&
|
||||
RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
|
||||
) {
|
||||
// hide chat in right panel when the widget is minimized
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
RightPanelStore.instance.togglePanel(this.state.roomId);
|
||||
}
|
||||
this.checkWidgets(this.state.room);
|
||||
};
|
||||
|
@ -571,8 +571,22 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
mainSplitContentType: room === null ? undefined : this.getMainSplitContentType(room),
|
||||
initialEventId: null, // default to clearing this, will get set later in the method if needed
|
||||
showRightPanel: RightPanelStore.instance.isOpenForRoom(roomId),
|
||||
activeCall: CallStore.instance.getActiveCall(roomId),
|
||||
};
|
||||
|
||||
if (
|
||||
this.state.mainSplitContentType !== MainSplitContentType.Timeline
|
||||
&& newState.mainSplitContentType === MainSplitContentType.Timeline
|
||||
&& RightPanelStore.instance.isOpen
|
||||
&& RightPanelStore.instance.currentCard.phase === RightPanelPhases.Timeline
|
||||
&& RightPanelStore.instance.roomPhaseHistory.some(card => (card.phase === RightPanelPhases.Timeline))
|
||||
) {
|
||||
// We're returning to the main timeline, so hide the right panel timeline
|
||||
RightPanelStore.instance.setCard({ phase: RightPanelPhases.RoomSummary });
|
||||
RightPanelStore.instance.togglePanel(this.state.roomId ?? null);
|
||||
newState.showRightPanel = false;
|
||||
}
|
||||
|
||||
const initialEventId = RoomViewStore.instance.getInitialEventId();
|
||||
if (initialEventId) {
|
||||
let initialEvent = room?.findEventById(initialEventId);
|
||||
|
@ -701,7 +715,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
};
|
||||
|
||||
private onActiveCalls = () => {
|
||||
if (this.state.roomId !== undefined && !CallStore.instance.hasActiveCall(this.state.roomId)) {
|
||||
if (this.state.roomId === undefined) return;
|
||||
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
|
||||
|
||||
if (activeCall === null) {
|
||||
// We disconnected from the call, so stop viewing it
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
|
@ -710,6 +727,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
metricsTrigger: undefined,
|
||||
}, true); // Synchronous so that CallView disappears immediately
|
||||
}
|
||||
|
||||
this.setState({ activeCall });
|
||||
};
|
||||
|
||||
private getRoomId = () => {
|
||||
|
@ -2404,6 +2423,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
let onForgetClick = this.onForgetClick;
|
||||
let onSearchClick = this.onSearchClick;
|
||||
let onInviteClick = null;
|
||||
let viewingCall = false;
|
||||
|
||||
// Simplify the header for other main split types
|
||||
switch (this.state.mainSplitContentType) {
|
||||
|
@ -2422,12 +2442,19 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
RightPanelPhases.PinnedMessages,
|
||||
RightPanelPhases.NotificationPanel,
|
||||
];
|
||||
if (!isVideoRoom(this.state.room)) {
|
||||
excludedRightPanelPhaseButtons.push(RightPanelPhases.RoomSummary);
|
||||
if (this.state.activeCall === null) {
|
||||
excludedRightPanelPhaseButtons.push(RightPanelPhases.Timeline);
|
||||
}
|
||||
}
|
||||
onAppsClick = null;
|
||||
onForgetClick = null;
|
||||
onSearchClick = null;
|
||||
if (this.state.room.canInvite(this.context.credentials.userId)) {
|
||||
onInviteClick = this.onInviteClick;
|
||||
}
|
||||
viewingCall = true;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -2451,6 +2478,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||
excludedRightPanelPhaseButtons={excludedRightPanelPhaseButtons}
|
||||
showButtons={!this.viewsLocalRoom}
|
||||
enableRoomOptionsMenu={!this.viewsLocalRoom}
|
||||
viewingCall={viewingCall}
|
||||
activeCall={this.state.activeCall}
|
||||
/>
|
||||
<MainSplit panel={rightPanel} resizeNotifier={this.props.resizeNotifier}>
|
||||
<div className={mainSplitContentClasses} ref={this.roomViewBody} data-layout={this.state.layout}>
|
||||
|
|
|
@ -493,7 +493,6 @@ export class EmailIdentityAuthEntry extends
|
|||
? _t("Resent!")
|
||||
: _t("Resend")}
|
||||
alignment={Alignment.Right}
|
||||
tooltipClassName="mx_Tooltip_noMargin"
|
||||
onHideTooltip={this.state.requested
|
||||
? () => this.setState({ requested: false })
|
||||
: undefined}
|
||||
|
|
|
@ -32,8 +32,10 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import { UIFeature } from "../../../settings/UIFeature";
|
||||
import BaseDialog from "./BaseDialog";
|
||||
import { Action } from '../../../dispatcher/actions';
|
||||
import { VoipRoomSettingsTab } from "../settings/tabs/room/VoipRoomSettingsTab";
|
||||
|
||||
export const ROOM_GENERAL_TAB = "ROOM_GENERAL_TAB";
|
||||
export const ROOM_VOIP_TAB = "ROOM_VOIP_TAB";
|
||||
export const ROOM_SECURITY_TAB = "ROOM_SECURITY_TAB";
|
||||
export const ROOM_ROLES_TAB = "ROOM_ROLES_TAB";
|
||||
export const ROOM_NOTIFICATIONS_TAB = "ROOM_NOTIFICATIONS_TAB";
|
||||
|
@ -96,6 +98,14 @@ export default class RoomSettingsDialog extends React.Component<IProps, IState>
|
|||
<GeneralRoomSettingsTab roomId={this.props.roomId} />,
|
||||
"RoomSettingsGeneral",
|
||||
));
|
||||
if (SettingsStore.getValue("feature_group_calls")) {
|
||||
tabs.push(new Tab(
|
||||
ROOM_VOIP_TAB,
|
||||
_td("Voice & Video"),
|
||||
"mx_RoomSettingsDialog_voiceIcon",
|
||||
<VoipRoomSettingsTab roomId={this.props.roomId} />,
|
||||
));
|
||||
}
|
||||
tabs.push(new Tab(
|
||||
ROOM_SECURITY_TAB,
|
||||
_td("Security & Privacy"),
|
||||
|
|
|
@ -21,7 +21,7 @@ import AccessibleButton from "./AccessibleButton";
|
|||
import Tooltip, { Alignment } from './Tooltip';
|
||||
|
||||
interface IProps extends React.ComponentProps<typeof AccessibleButton> {
|
||||
title: string;
|
||||
title?: string;
|
||||
tooltip?: React.ReactNode;
|
||||
label?: string;
|
||||
tooltipClassName?: string;
|
||||
|
@ -78,7 +78,7 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
|||
const { title, tooltip, children, tooltipClassName, forceHide, alignment, onHideTooltip,
|
||||
...props } = this.props;
|
||||
|
||||
const tip = this.state.hover && <Tooltip
|
||||
const tip = this.state.hover && (title || tooltip) && <Tooltip
|
||||
tooltipClassName={tooltipClassName}
|
||||
label={tooltip || title}
|
||||
alignment={alignment}
|
||||
|
@ -86,11 +86,11 @@ export default class AccessibleTooltipButton extends React.PureComponent<IProps,
|
|||
return (
|
||||
<AccessibleButton
|
||||
{...props}
|
||||
onMouseOver={this.showTooltip}
|
||||
onMouseLeave={this.hideTooltip}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.hideTooltip}
|
||||
aria-label={title}
|
||||
onMouseOver={this.showTooltip || props.onMouseOver}
|
||||
onMouseLeave={this.hideTooltip || props.onMouseLeave}
|
||||
onFocus={this.onFocus || props.onFocus}
|
||||
onBlur={this.hideTooltip || props.onBlur}
|
||||
aria-label={title || props["aria-label"]}
|
||||
>
|
||||
{ children }
|
||||
{ this.props.label }
|
||||
|
|
|
@ -27,6 +27,8 @@ interface IProps {
|
|||
label: string;
|
||||
// The translated caption for the switch
|
||||
caption?: string;
|
||||
// Tooltip to display
|
||||
tooltip?: string;
|
||||
// Whether or not to disable the toggle switch
|
||||
disabled?: boolean;
|
||||
// True to put the toggle in front of the label
|
||||
|
@ -53,7 +55,8 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
|||
checked={this.props.value}
|
||||
disabled={this.props.disabled}
|
||||
onChange={this.props.onChange}
|
||||
aria-label={this.props.label}
|
||||
title={this.props.label}
|
||||
tooltip={this.props.tooltip}
|
||||
/>;
|
||||
|
||||
if (this.props.toggleInFront) {
|
||||
|
@ -66,7 +69,7 @@ export default class LabelledToggleSwitch extends React.PureComponent<IProps> {
|
|||
"mx_SettingsFlag_toggleInFront": this.props.toggleInFront,
|
||||
});
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div data-testid={this.props["data-testid"]} className={classes}>
|
||||
{ firstPart }
|
||||
{ secondPart }
|
||||
</div>
|
||||
|
|
|
@ -114,7 +114,7 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||
checked={this.state.value}
|
||||
onChange={this.onChange}
|
||||
disabled={this.props.disabled || !canChange}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -18,21 +18,27 @@ limitations under the License.
|
|||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import AccessibleTooltipButton from "./AccessibleTooltipButton";
|
||||
|
||||
interface IProps {
|
||||
// Whether or not this toggle is in the 'on' position.
|
||||
checked: boolean;
|
||||
|
||||
// Title to use
|
||||
title?: string;
|
||||
|
||||
// Whether or not the user can interact with the switch
|
||||
disabled?: boolean;
|
||||
|
||||
// Tooltip to show
|
||||
tooltip?: string;
|
||||
|
||||
// Called when the checked state changes. First argument will be the new state.
|
||||
onChange(checked: boolean): void;
|
||||
}
|
||||
|
||||
// Controlled Toggle Switch element, written with Accessibility in mind
|
||||
export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
||||
export default ({ checked, disabled = false, title, tooltip, onChange, ...props }: IProps) => {
|
||||
const _onClick = () => {
|
||||
if (disabled) return;
|
||||
onChange(!checked);
|
||||
|
@ -45,14 +51,16 @@ export default ({ checked, disabled = false, onChange, ...props }: IProps) => {
|
|||
});
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props}
|
||||
<AccessibleTooltipButton {...props}
|
||||
className={classes}
|
||||
onClick={_onClick}
|
||||
role="switch"
|
||||
aria-checked={checked}
|
||||
aria-disabled={disabled}
|
||||
title={title}
|
||||
tooltip={tooltip}
|
||||
>
|
||||
<div className="mx_ToggleSwitch_ball" />
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -149,18 +149,24 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
|
|||
break;
|
||||
case Alignment.Top:
|
||||
style.top = baseTop - spacing;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%, -100%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))), -100%)`;
|
||||
break;
|
||||
case Alignment.Bottom:
|
||||
style.top = baseTop + parentBox.height + spacing;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
|
||||
break;
|
||||
case Alignment.InnerBottom:
|
||||
style.top = baseTop + parentBox.height - 50;
|
||||
style.left = horizontalCenter;
|
||||
style.transform = "translate(-50%)";
|
||||
// Attempt to center the tooltip on the element while clamping
|
||||
// its horizontal translation to keep it on screen
|
||||
// eslint-disable-next-line max-len
|
||||
style.transform = `translate(max(10px, min(calc(${horizontalCenter}px - 50%), calc(100vw - 100% - 10px))))`;
|
||||
break;
|
||||
case Alignment.TopRight:
|
||||
style.top = baseTop - spacing;
|
||||
|
|
|
@ -20,7 +20,7 @@ import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { Call, ConnectionState } from "../../../models/Call";
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { useCall, useConnectionState, useParticipants } from "../../../hooks/useCall";
|
||||
import { useCall, useConnectionState, useJoinCallButtonDisabledTooltip, useParticipants } from "../../../hooks/useCall";
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import type { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
|
@ -28,9 +28,9 @@ import type { ButtonEvent } from "../elements/AccessibleButton";
|
|||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
import { LiveContentSummary, LiveContentType } from "../rooms/LiveContentSummary";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { CallDuration, CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
|
||||
const MAX_FACES = 8;
|
||||
|
||||
|
@ -39,6 +39,8 @@ interface ActiveCallEventProps {
|
|||
participants: Set<RoomMember>;
|
||||
buttonText: string;
|
||||
buttonKind: string;
|
||||
buttonTooltip?: string;
|
||||
buttonDisabled?: boolean;
|
||||
onButtonClick: ((ev: ButtonEvent) => void) | null;
|
||||
}
|
||||
|
||||
|
@ -49,6 +51,8 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
|||
participants,
|
||||
buttonText,
|
||||
buttonKind,
|
||||
buttonDisabled,
|
||||
buttonTooltip,
|
||||
onButtonClick,
|
||||
},
|
||||
ref,
|
||||
|
@ -80,14 +84,15 @@ const ActiveCallEvent = forwardRef<any, ActiveCallEventProps>(
|
|||
<FacePile members={facePileMembers} faceSize={24} overflow={facePileOverflow} />
|
||||
</div>
|
||||
<CallDurationFromEvent mxEvent={mxEvent} />
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallEvent_button"
|
||||
kind={buttonKind}
|
||||
disabled={onButtonClick === null}
|
||||
disabled={onButtonClick === null || buttonDisabled}
|
||||
onClick={onButtonClick}
|
||||
tooltip={buttonTooltip}
|
||||
>
|
||||
{ buttonText }
|
||||
</AccessibleButton>
|
||||
</AccessibleTooltipButton>
|
||||
</div>
|
||||
</div>;
|
||||
},
|
||||
|
@ -101,6 +106,7 @@ interface ActiveLoadedCallEventProps {
|
|||
const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxEvent, call }, ref) => {
|
||||
const connectionState = useConnectionState(call);
|
||||
const participants = useParticipants(call);
|
||||
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
|
||||
const connect = useCallback((ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
|
@ -132,6 +138,8 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
|
|||
participants={participants}
|
||||
buttonText={buttonText}
|
||||
buttonKind={buttonKind}
|
||||
buttonDisabled={Boolean(joinCallButtonDisabledTooltip)}
|
||||
buttonTooltip={joinCallButtonDisabledTooltip}
|
||||
onButtonClick={onButtonClick}
|
||||
/>;
|
||||
});
|
||||
|
|
|
@ -23,6 +23,7 @@ import classNames from 'classnames';
|
|||
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
interface IProps {
|
||||
// Whether this button is highlighted
|
||||
|
@ -54,6 +55,7 @@ export default class HeaderButton extends React.Component<IProps> {
|
|||
aria-selected={isHighlighted}
|
||||
role="tab"
|
||||
title={title}
|
||||
alignment={Alignment.Bottom}
|
||||
className={classes}
|
||||
onClick={onClick}
|
||||
/>;
|
||||
|
|
|
@ -282,7 +282,7 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
|||
<HeaderButton
|
||||
key="roomSummaryButton"
|
||||
name="roomSummaryButton"
|
||||
title={_t('Room Info')}
|
||||
title={_t('Room info')}
|
||||
isHighlighted={this.isPhase(ROOM_INFO_PHASES)}
|
||||
onClick={this.onRoomSummaryClicked}
|
||||
/>,
|
||||
|
|
|
@ -31,7 +31,6 @@ import Resizer from "../../../resizer/resizer";
|
|||
import PercentageDistributor from "../../../resizer/distributors/percentage";
|
||||
import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
|
||||
import { clamp, percentageOf, percentageWithin } from "../../../utils/numbers";
|
||||
import { useStateCallback } from "../../../hooks/useStateCallback";
|
||||
import UIStore from "../../../stores/UIStore";
|
||||
import { IApp } from "../../../stores/WidgetStore";
|
||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||
|
@ -330,13 +329,8 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||
defaultHeight = 280;
|
||||
}
|
||||
|
||||
const [height, setHeight] = useStateCallback(defaultHeight, newHeight => {
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
WidgetLayoutStore.instance.setContainerHeight(room, Container.Top, newHeight);
|
||||
});
|
||||
|
||||
return <Resizable
|
||||
size={{ height: Math.min(height, maxHeight), width: undefined }}
|
||||
size={{ height: Math.min(defaultHeight, maxHeight), width: undefined }}
|
||||
minHeight={minHeight}
|
||||
maxHeight={maxHeight}
|
||||
onResizeStart={() => {
|
||||
|
@ -346,7 +340,15 @@ const PersistentVResizer: React.FC<IPersistentResizerProps> = ({
|
|||
resizeNotifier.notifyTimelineHeightChanged();
|
||||
}}
|
||||
onResizeStop={(e, dir, ref, d) => {
|
||||
setHeight(height + d.height);
|
||||
let newHeight = defaultHeight + d.height;
|
||||
newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100;
|
||||
|
||||
WidgetLayoutStore.instance.setContainerHeight(
|
||||
room,
|
||||
Container.Top,
|
||||
newHeight,
|
||||
);
|
||||
|
||||
resizeNotifier.stopResizing();
|
||||
}}
|
||||
handleWrapperClass={handleWrapperClass}
|
||||
|
|
|
@ -20,7 +20,7 @@ import classNames from 'classnames';
|
|||
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Tooltip from "../elements/Tooltip";
|
||||
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||
|
||||
export enum E2EState {
|
||||
|
@ -49,10 +49,20 @@ interface IProps {
|
|||
size?: number;
|
||||
onClick?: () => void;
|
||||
hideTooltip?: boolean;
|
||||
tooltipAlignment?: Alignment;
|
||||
bordered?: boolean;
|
||||
}
|
||||
|
||||
const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, hideTooltip, bordered }) => {
|
||||
const E2EIcon: React.FC<IProps> = ({
|
||||
isUser,
|
||||
status,
|
||||
className,
|
||||
size,
|
||||
onClick,
|
||||
hideTooltip,
|
||||
tooltipAlignment,
|
||||
bordered,
|
||||
}) => {
|
||||
const [hover, setHover] = useState(false);
|
||||
|
||||
const classes = classNames({
|
||||
|
@ -80,7 +90,7 @@ const E2EIcon: React.FC<IProps> = ({ isUser, status, className, size, onClick, h
|
|||
|
||||
let tip;
|
||||
if (hover && !hideTooltip) {
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} alignment={tooltipAlignment} />;
|
||||
}
|
||||
|
||||
if (onClick) {
|
||||
|
|
|
@ -18,6 +18,8 @@ import React, { FC } from "react";
|
|||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import { Call } from "../../../models/Call";
|
||||
import { useParticipants } from "../../../hooks/useCall";
|
||||
|
||||
export enum LiveContentType {
|
||||
Video,
|
||||
|
@ -55,3 +57,18 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
|
|||
</> }
|
||||
</span>
|
||||
);
|
||||
|
||||
interface LiveContentSummaryWithCallProps {
|
||||
call: Call;
|
||||
}
|
||||
|
||||
export function LiveContentSummaryWithCall({ call }: LiveContentSummaryWithCallProps) {
|
||||
const participants = useParticipants(call);
|
||||
|
||||
return <LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={_t("Video")}
|
||||
active={false}
|
||||
participantCount={participants.size}
|
||||
/>;
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import { CallType } from "matrix-js-sdk/src/webrtc/call";
|
|||
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 { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||
import { Action } from "../../../dispatcher/actions";
|
||||
import { UserTab } from "../dialogs/UserTab";
|
||||
|
@ -32,7 +31,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
|||
import RoomHeaderButtons from '../right_panel/RoomHeaderButtons';
|
||||
import E2EIcon from './E2EIcon';
|
||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import RoomTopic from "../elements/RoomTopic";
|
||||
import RoomName from "../elements/RoomName";
|
||||
|
@ -53,18 +52,21 @@ 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 SdkConfig, { DEFAULTS } from "../../../SdkConfig";
|
||||
import { useEventEmitterState, useTypedEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
import { useWidgets } from "../right_panel/RoomSummaryCard";
|
||||
import { WidgetType } from "../../../widgets/WidgetType";
|
||||
import { useCall } from "../../../hooks/useCall";
|
||||
import { useCall, useLayout } from "../../../hooks/useCall";
|
||||
import { getJoinedNonFunctionalMembers } from "../../../utils/room/getJoinedNonFunctionalMembers";
|
||||
import { ElementCall } from "../../../models/Call";
|
||||
import { Call, ElementCall, Layout } from "../../../models/Call";
|
||||
import IconizedContextMenu, {
|
||||
IconizedContextMenuOption,
|
||||
IconizedContextMenuOptionList,
|
||||
IconizedContextMenuRadio,
|
||||
} from "../context_menus/IconizedContextMenu";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { CallDurationFromEvent } from "../voip/CallDuration";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
|
||||
class DisabledWithReason {
|
||||
constructor(public readonly reason: string) { }
|
||||
|
@ -107,6 +109,7 @@ const VoiceCallButton: FC<VoiceCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
onClick={onClick}
|
||||
title={_t("Voice call")}
|
||||
tooltip={tooltip ?? _t("Voice call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>;
|
||||
};
|
||||
|
@ -192,10 +195,11 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
let menu: JSX.Element | null = null;
|
||||
if (menuOpen) {
|
||||
const buttonRect = buttonRef.current!.getBoundingClientRect();
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
menu = <IconizedContextMenu {...aboveLeftOf(buttonRect)} onFinished={closeMenu}>
|
||||
<IconizedContextMenuOptionList>
|
||||
<IconizedContextMenuOption label={_t("Video call (Jitsi)")} onClick={onJitsiClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (Element Call)")} onClick={onElementClick} />
|
||||
<IconizedContextMenuOption label={_t("Video call (%(brand)s)", { brand })} onClick={onElementClick} />
|
||||
</IconizedContextMenuOptionList>
|
||||
</IconizedContextMenu>;
|
||||
}
|
||||
|
@ -207,6 +211,7 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
|
|||
onClick={onClick}
|
||||
title={_t("Video call")}
|
||||
tooltip={tooltip ?? _t("Video call")}
|
||||
alignment={Alignment.Bottom}
|
||||
disabled={disabled || busy}
|
||||
/>
|
||||
{ menu }
|
||||
|
@ -225,7 +230,9 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|||
const groupCallsEnabled = useFeatureEnabled("feature_group_calls");
|
||||
const videoRoomsEnabled = useFeatureEnabled("feature_video_rooms");
|
||||
const isVideoRoom = useMemo(() => videoRoomsEnabled && calcIsVideoRoom(room), [videoRoomsEnabled, room]);
|
||||
const useElementCallExclusively = useMemo(() => SdkConfig.get("element_call").use_exclusively, []);
|
||||
const useElementCallExclusively = useMemo(() => {
|
||||
return SdkConfig.get("element_call").use_exclusively ?? DEFAULTS.element_call.use_exclusively;
|
||||
}, []);
|
||||
|
||||
const hasLegacyCall = useEventEmitterState(
|
||||
LegacyCallHandler.instance,
|
||||
|
@ -318,6 +325,72 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
|
|||
}
|
||||
};
|
||||
|
||||
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("Layout type")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="layout"
|
||||
/>
|
||||
{ menu }
|
||||
</>;
|
||||
};
|
||||
|
||||
export interface ISearchInfo {
|
||||
searchTerm: string;
|
||||
searchScope: SearchScope;
|
||||
|
@ -338,6 +411,8 @@ export interface IProps {
|
|||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||
showButtons?: boolean;
|
||||
enableRoomOptionsMenu?: boolean;
|
||||
viewingCall: boolean;
|
||||
activeCall: Call | null;
|
||||
}
|
||||
|
||||
interface IState {
|
||||
|
@ -356,6 +431,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
|
||||
static contextType = RoomContext;
|
||||
public context!: React.ContextType<typeof RoomContext>;
|
||||
private readonly client = this.props.room.client;
|
||||
|
||||
constructor(props: IProps, context: IState) {
|
||||
super(props, context);
|
||||
|
@ -367,14 +443,12 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
this.client.on(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
cli?.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
|
||||
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);
|
||||
|
@ -401,7 +475,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
this.forceUpdate();
|
||||
}, 500, { leading: true, trailing: true });
|
||||
|
||||
private onContextMenuOpenClick = (ev: React.MouseEvent) => {
|
||||
private onContextMenuOpenClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const target = ev.target as HTMLButtonElement;
|
||||
|
@ -412,56 +486,98 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
this.setState({ contextMenuPosition: undefined });
|
||||
};
|
||||
|
||||
private renderButtons(): JSX.Element[] {
|
||||
const buttons: JSX.Element[] = [];
|
||||
private onHideCallClick = (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.props.room.roomId,
|
||||
view_call: false,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
if (this.props.inRoom && !this.context.tombstone) {
|
||||
buttons.push(<CallButtons key="calls" room={this.props.room} />);
|
||||
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.onForgetClick) {
|
||||
const forgetButton = <AccessibleTooltipButton
|
||||
if (this.props.viewingCall && this.props.activeCall instanceof ElementCall) {
|
||||
startButtons.push(<CallLayoutSelector 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"
|
||||
/>;
|
||||
buttons.push(forgetButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onAppsClick) {
|
||||
const appsButton = <AccessibleTooltipButton
|
||||
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")}
|
||||
alignment={Alignment.Bottom}
|
||||
key="apps"
|
||||
/>;
|
||||
buttons.push(appsButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onSearchClick && this.props.inRoom) {
|
||||
const searchButton = <AccessibleTooltipButton
|
||||
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"
|
||||
/>;
|
||||
buttons.push(searchButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
if (this.props.onInviteClick && this.props.inRoom) {
|
||||
const inviteButton = <AccessibleTooltipButton
|
||||
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"
|
||||
/>;
|
||||
buttons.push(inviteButton);
|
||||
/>);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
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) {
|
||||
|
@ -480,7 +596,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
let settingsHint = false;
|
||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||
if (members) {
|
||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||
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;
|
||||
|
@ -505,6 +621,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
onClick={this.onContextMenuOpenClick}
|
||||
isExpanded={!!this.state.contextMenuPosition}
|
||||
title={_t("Room options")}
|
||||
alignment={Alignment.Bottom}
|
||||
>
|
||||
{ roomName }
|
||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||
|
@ -519,6 +636,57 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
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;
|
||||
|
||||
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 }
|
||||
<div className="mx_RoomHeader_name mx_RoomHeader_name--textonly mx_RoomHeader_name--small">
|
||||
{ _t("Video call") }
|
||||
</div>
|
||||
{ this.props.activeCall instanceof ElementCall && (
|
||||
<CallDurationFromEvent mxEvent={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
|
||||
|
@ -543,29 +711,6 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
className="mx_RoomHeader_topic"
|
||||
/>;
|
||||
|
||||
let roomAvatar: JSX.Element | null = null;
|
||||
if (this.props.room) {
|
||||
roomAvatar = <DecoratedRoomAvatar
|
||||
room={this.props.room}
|
||||
avatarSize={24}
|
||||
oobData={this.props.oobData}
|
||||
viewAvatarOnClick={true}
|
||||
/>;
|
||||
}
|
||||
|
||||
let buttons: JSX.Element | null = null;
|
||||
if (this.props.showButtons) {
|
||||
buttons = <React.Fragment>
|
||||
<div className="mx_RoomHeader_buttons">
|
||||
{ this.renderButtons() }
|
||||
</div>
|
||||
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||
|
||||
const isVideoRoom = SettingsStore.getValue("feature_video_rooms") && calcIsVideoRoom(this.props.room);
|
||||
const viewLabs = () => defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: UserTab.Labs,
|
||||
|
@ -581,7 +726,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
|||
aria-owns={this.state.rightPanelOpen ? "mx_RightPanel" : undefined}
|
||||
>
|
||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }</div>
|
||||
<div className="mx_RoomHeader_e2eIcon">{ e2eIcon }</div>
|
||||
{ icon }
|
||||
{ name }
|
||||
{ searchStatus }
|
||||
{ topicElement }
|
||||
|
|
|
@ -89,7 +89,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
selected: RoomViewStore.instance.getRoomId() === this.props.room.roomId,
|
||||
notificationsMenuPosition: null,
|
||||
generalMenuPosition: null,
|
||||
call: CallStore.instance.get(this.props.room.roomId),
|
||||
call: CallStore.instance.getCall(this.props.room.roomId),
|
||||
// generatePreview() will return nothing if the user has previews disabled
|
||||
messagePreview: "",
|
||||
};
|
||||
|
@ -159,7 +159,7 @@ export default class RoomTile extends React.PureComponent<IProps, IState> {
|
|||
|
||||
// Recalculate the call for this room, since it could've changed between
|
||||
// construction and mounting
|
||||
this.setState({ call: CallStore.instance.get(this.props.room.roomId) });
|
||||
this.setState({ call: CallStore.instance.getCall(this.props.room.roomId) });
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
|
|
|
@ -29,6 +29,7 @@ import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDi
|
|||
import LogoutDialog from '../dialogs/LogoutDialog';
|
||||
import DeviceTile from './devices/DeviceTile';
|
||||
import SelectableDeviceTile from './devices/SelectableDeviceTile';
|
||||
import { DeviceType } from '../../../utils/device/parseUserAgent';
|
||||
|
||||
interface IProps {
|
||||
device: IMyDevice;
|
||||
|
@ -153,9 +154,10 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
|||
</AccessibleButton>
|
||||
</React.Fragment>;
|
||||
|
||||
const deviceWithVerification = {
|
||||
const extendedDevice = {
|
||||
...this.props.device,
|
||||
isVerified: this.props.verified,
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
|
||||
if (this.props.isOwnDevice) {
|
||||
|
@ -163,7 +165,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
|||
<div className="mx_DevicesPanel_deviceTrust">
|
||||
<span className={"mx_DevicesPanel_icon mx_E2EIcon " + iconClass} />
|
||||
</div>
|
||||
<DeviceTile device={deviceWithVerification}>
|
||||
<DeviceTile device={extendedDevice}>
|
||||
{ buttons }
|
||||
</DeviceTile>
|
||||
</div>;
|
||||
|
@ -171,7 +173,7 @@ export default class DevicesPanelEntry extends React.Component<IProps, IState> {
|
|||
|
||||
return (
|
||||
<div className="mx_DevicesPanel_device">
|
||||
<SelectableDeviceTile device={deviceWithVerification} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
|
||||
<SelectableDeviceTile device={extendedDevice} onClick={this.onDeviceToggled} isSelected={this.props.selected}>
|
||||
{ buttons }
|
||||
</SelectableDeviceTile>
|
||||
</div>
|
||||
|
|
|
@ -24,10 +24,10 @@ import DeviceDetails from './DeviceDetails';
|
|||
import DeviceExpandDetailsButton from './DeviceExpandDetailsButton';
|
||||
import DeviceTile from './DeviceTile';
|
||||
import { DeviceVerificationStatusCard } from './DeviceVerificationStatusCard';
|
||||
import { DeviceWithVerification } from './types';
|
||||
import { ExtendedDevice } from './types';
|
||||
|
||||
interface Props {
|
||||
device?: DeviceWithVerification;
|
||||
device?: ExtendedDevice;
|
||||
isLoading: boolean;
|
||||
isSigningOut: boolean;
|
||||
localNotificationSettings?: LocalNotificationSettings | undefined;
|
||||
|
|
|
@ -22,10 +22,10 @@ import Field from '../../elements/Field';
|
|||
import Spinner from '../../elements/Spinner';
|
||||
import { Caption } from '../../typography/Caption';
|
||||
import Heading from '../../typography/Heading';
|
||||
import { DeviceWithVerification } from './types';
|
||||
import { ExtendedDevice } from './types';
|
||||
|
||||
interface Props {
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
saveDeviceName: (deviceName: string) => Promise<void>;
|
||||
}
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'session',
|
||||
values: [
|
||||
{ label: _t('Session ID'), value: device.device_id },
|
||||
{ label: _t('Client'), value: device.client },
|
||||
{
|
||||
label: _t('Last activity'),
|
||||
value: device.last_seen_ts && formatDate(new Date(device.last_seen_ts)),
|
||||
|
@ -72,8 +73,8 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'application',
|
||||
heading: _t('Application'),
|
||||
values: [
|
||||
{ label: _t('Name'), value: device.clientName },
|
||||
{ label: _t('Version'), value: device.clientVersion },
|
||||
{ label: _t('Name'), value: device.appName },
|
||||
{ label: _t('Version'), value: device.appVersion },
|
||||
{ label: _t('URL'), value: device.url },
|
||||
],
|
||||
},
|
||||
|
@ -81,6 +82,8 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
id: 'device',
|
||||
heading: _t('Device'),
|
||||
values: [
|
||||
{ label: _t('Model'), value: device.deviceModel },
|
||||
{ label: _t('Operating system'), value: device.deviceOperatingSystem },
|
||||
{ label: _t('IP address'), value: device.last_seen_ip },
|
||||
],
|
||||
},
|
||||
|
@ -150,7 +153,7 @@ const DeviceDetails: React.FC<Props> = ({
|
|||
checked={isPushNotificationsEnabled(pusher, localNotificationSettings)}
|
||||
disabled={isCheckboxDisabled(pusher, localNotificationSettings)}
|
||||
onChange={checked => setPushNotifications?.(device.device_id, checked)}
|
||||
aria-label={_t("Toggle push notifications on this session.")}
|
||||
title={_t("Toggle push notifications on this session.")}
|
||||
data-testid='device-detail-push-notification-checkbox'
|
||||
/>
|
||||
<p className='mx_DeviceDetails_sectionHeading'>
|
||||
|
|
|
@ -21,16 +21,16 @@ import { _t } from "../../../../languageHandler";
|
|||
import { formatDate, formatRelativeTime } from "../../../../DateUtils";
|
||||
import Heading from "../../typography/Heading";
|
||||
import { INACTIVE_DEVICE_AGE_DAYS, isDeviceInactive } from "./filter";
|
||||
import { DeviceWithVerification } from "./types";
|
||||
import { DeviceType } from "./DeviceType";
|
||||
import { ExtendedDevice } from "./types";
|
||||
import { DeviceTypeIcon } from "./DeviceTypeIcon";
|
||||
export interface DeviceTileProps {
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
isSelected?: boolean;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
const DeviceTileName: React.FC<{ device: DeviceWithVerification }> = ({ device }) => {
|
||||
const DeviceTileName: React.FC<{ device: ExtendedDevice }> = ({ device }) => {
|
||||
return <Heading size='h4'>
|
||||
{ device.display_name || device.device_id }
|
||||
</Heading>;
|
||||
|
@ -48,7 +48,7 @@ const formatLastActivity = (timestamp: number, now = new Date().getTime()): stri
|
|||
return formatRelativeTime(new Date(timestamp));
|
||||
};
|
||||
|
||||
const getInactiveMetadata = (device: DeviceWithVerification): { id: string, value: React.ReactNode } | undefined => {
|
||||
const getInactiveMetadata = (device: ExtendedDevice): { id: string, value: React.ReactNode } | undefined => {
|
||||
const isInactive = isDeviceInactive(device);
|
||||
|
||||
if (!isInactive) {
|
||||
|
@ -89,7 +89,11 @@ const DeviceTile: React.FC<DeviceTileProps> = ({
|
|||
];
|
||||
|
||||
return <div className="mx_DeviceTile" data-testid={`device-tile-${device.device_id}`}>
|
||||
<DeviceType isVerified={device.isVerified} isSelected={isSelected} />
|
||||
<DeviceTypeIcon
|
||||
isVerified={device.isVerified}
|
||||
isSelected={isSelected}
|
||||
deviceType={device.deviceType}
|
||||
/>
|
||||
<div className="mx_DeviceTile_info" onClick={onClick}>
|
||||
<DeviceTileName device={device} />
|
||||
<div className="mx_DeviceTile_metadata">
|
||||
|
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
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 classNames from 'classnames';
|
||||
|
||||
import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg';
|
||||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { DeviceWithVerification } from './types';
|
||||
|
||||
interface Props {
|
||||
isVerified?: DeviceWithVerification['isVerified'];
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
export const DeviceType: React.FC<Props> = ({ isVerified, isSelected }) => (
|
||||
<div className={classNames('mx_DeviceType', {
|
||||
mx_DeviceType_selected: isSelected,
|
||||
})}
|
||||
>
|
||||
{ /* TODO(kerrya) all devices have an unknown type until PSG-650 */ }
|
||||
<UnknownDeviceIcon
|
||||
className='mx_DeviceType_deviceIcon'
|
||||
role='img'
|
||||
aria-label={_t('Unknown device type')}
|
||||
/>
|
||||
{
|
||||
isVerified
|
||||
? <VerifiedIcon
|
||||
className={classNames('mx_DeviceType_verificationIcon', 'verified')}
|
||||
role='img'
|
||||
aria-label={_t('Verified')}
|
||||
/>
|
||||
: <UnverifiedIcon
|
||||
className={classNames('mx_DeviceType_verificationIcon', 'unverified')}
|
||||
role='img'
|
||||
aria-label={_t('Unverified')}
|
||||
/>
|
||||
}
|
||||
</div>);
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
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 classNames from 'classnames';
|
||||
|
||||
import { Icon as UnknownDeviceIcon } from '../../../../../res/img/element-icons/settings/unknown-device.svg';
|
||||
import { Icon as DesktopIcon } from '../../../../../res/img/element-icons/settings/desktop.svg';
|
||||
import { Icon as WebIcon } from '../../../../../res/img/element-icons/settings/web.svg';
|
||||
import { Icon as MobileIcon } from '../../../../../res/img/element-icons/settings/mobile.svg';
|
||||
import { Icon as VerifiedIcon } from '../../../../../res/img/e2e/verified.svg';
|
||||
import { Icon as UnverifiedIcon } from '../../../../../res/img/e2e/warning.svg';
|
||||
import { _t } from '../../../../languageHandler';
|
||||
import { ExtendedDevice } from './types';
|
||||
import { DeviceType } from '../../../../utils/device/parseUserAgent';
|
||||
|
||||
interface Props {
|
||||
isVerified?: ExtendedDevice['isVerified'];
|
||||
isSelected?: boolean;
|
||||
deviceType?: DeviceType;
|
||||
}
|
||||
|
||||
const deviceTypeIcon: Record<DeviceType, React.FC<React.SVGProps<SVGSVGElement>>> = {
|
||||
[DeviceType.Desktop]: DesktopIcon,
|
||||
[DeviceType.Mobile]: MobileIcon,
|
||||
[DeviceType.Web]: WebIcon,
|
||||
[DeviceType.Unknown]: UnknownDeviceIcon,
|
||||
};
|
||||
const deviceTypeLabel: Record<DeviceType, string> = {
|
||||
[DeviceType.Desktop]: _t('Desktop session'),
|
||||
[DeviceType.Mobile]: _t('Mobile session'),
|
||||
[DeviceType.Web]: _t('Web session'),
|
||||
[DeviceType.Unknown]: _t('Unknown session type'),
|
||||
};
|
||||
|
||||
export const DeviceTypeIcon: React.FC<Props> = ({
|
||||
isVerified,
|
||||
isSelected,
|
||||
deviceType,
|
||||
}) => {
|
||||
const Icon = deviceTypeIcon[deviceType] || deviceTypeIcon[DeviceType.Unknown];
|
||||
const label = deviceTypeLabel[deviceType] || deviceTypeLabel[DeviceType.Unknown];
|
||||
return (
|
||||
<div className={classNames('mx_DeviceTypeIcon', {
|
||||
mx_DeviceTypeIcon_selected: isSelected,
|
||||
})}
|
||||
>
|
||||
<div className='mx_DeviceTypeIcon_deviceIconWrapper'>
|
||||
<Icon
|
||||
className='mx_DeviceTypeIcon_deviceIcon'
|
||||
role='img'
|
||||
aria-label={label}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
isVerified
|
||||
? <VerifiedIcon
|
||||
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'verified')}
|
||||
role='img'
|
||||
aria-label={_t('Verified')}
|
||||
/>
|
||||
: <UnverifiedIcon
|
||||
className={classNames('mx_DeviceTypeIcon_verificationIcon', 'unverified')}
|
||||
role='img'
|
||||
aria-label={_t('Unverified')}
|
||||
/>
|
||||
}
|
||||
</div>);
|
||||
};
|
||||
|
|
@ -21,11 +21,11 @@ import AccessibleButton from '../../elements/AccessibleButton';
|
|||
import DeviceSecurityCard from './DeviceSecurityCard';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
ExtendedDevice,
|
||||
} from './types';
|
||||
|
||||
interface Props {
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
onVerifyDevice?: () => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -33,7 +33,7 @@ import SelectableDeviceTile from './SelectableDeviceTile';
|
|||
import {
|
||||
DevicesDictionary,
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
ExtendedDevice,
|
||||
} from './types';
|
||||
import { DevicesState } from './useOwnDevices';
|
||||
import FilteredDeviceListHeader from './FilteredDeviceListHeader';
|
||||
|
@ -42,27 +42,27 @@ interface Props {
|
|||
devices: DevicesDictionary;
|
||||
pushers: IPusher[];
|
||||
localNotificationSettings: Map<string, LocalNotificationSettings>;
|
||||
expandedDeviceIds: DeviceWithVerification['device_id'][];
|
||||
signingOutDeviceIds: DeviceWithVerification['device_id'][];
|
||||
selectedDeviceIds: DeviceWithVerification['device_id'][];
|
||||
expandedDeviceIds: ExtendedDevice['device_id'][];
|
||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][];
|
||||
filter?: DeviceSecurityVariation;
|
||||
onFilterChange: (filter: DeviceSecurityVariation | undefined) => void;
|
||||
onDeviceExpandToggle: (deviceId: DeviceWithVerification['device_id']) => void;
|
||||
onSignOutDevices: (deviceIds: DeviceWithVerification['device_id'][]) => void;
|
||||
onDeviceExpandToggle: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
onSignOutDevices: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
saveDeviceName: DevicesState['saveDeviceName'];
|
||||
onRequestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => void;
|
||||
onRequestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => void;
|
||||
setPushNotifications: (deviceId: string, enabled: boolean) => Promise<void>;
|
||||
setSelectedDeviceIds: (deviceIds: DeviceWithVerification['device_id'][]) => void;
|
||||
setSelectedDeviceIds: (deviceIds: ExtendedDevice['device_id'][]) => void;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
}
|
||||
|
||||
const isDeviceSelected = (
|
||||
deviceId: DeviceWithVerification['device_id'],
|
||||
selectedDeviceIds: DeviceWithVerification['device_id'][],
|
||||
deviceId: ExtendedDevice['device_id'],
|
||||
selectedDeviceIds: ExtendedDevice['device_id'][],
|
||||
) => selectedDeviceIds.includes(deviceId);
|
||||
|
||||
// devices without timestamp metadata should be sorted last
|
||||
const sortDevicesByLatestActivity = (left: DeviceWithVerification, right: DeviceWithVerification) =>
|
||||
const sortDevicesByLatestActivity = (left: ExtendedDevice, right: ExtendedDevice) =>
|
||||
(right.last_seen_ts || 0) - (left.last_seen_ts || 0);
|
||||
|
||||
const getFilteredSortedDevices = (devices: DevicesDictionary, filter?: DeviceSecurityVariation) =>
|
||||
|
@ -149,7 +149,7 @@ const NoResults: React.FC<NoResultsProps> = ({ filter, clearFilter }) =>
|
|||
</div>;
|
||||
|
||||
const DeviceListItem: React.FC<{
|
||||
device: DeviceWithVerification;
|
||||
device: ExtendedDevice;
|
||||
pusher?: IPusher | undefined;
|
||||
localNotificationSettings?: LocalNotificationSettings | undefined;
|
||||
isExpanded: boolean;
|
||||
|
@ -227,11 +227,11 @@ export const FilteredDeviceList =
|
|||
}: Props, ref: ForwardedRef<HTMLDivElement>) => {
|
||||
const sortedDevices = getFilteredSortedDevices(devices, filter);
|
||||
|
||||
function getPusherForDevice(device: DeviceWithVerification): IPusher | undefined {
|
||||
function getPusherForDevice(device: ExtendedDevice): IPusher | undefined {
|
||||
return pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === device.device_id);
|
||||
}
|
||||
|
||||
const toggleSelection = (deviceId: DeviceWithVerification['device_id']): void => {
|
||||
const toggleSelection = (deviceId: ExtendedDevice['device_id']): void => {
|
||||
if (isDeviceSelected(deviceId, selectedDeviceIds)) {
|
||||
// remove from selection
|
||||
setSelectedDeviceIds(selectedDeviceIds.filter(id => id !== deviceId));
|
||||
|
|
|
@ -23,13 +23,13 @@ import DeviceSecurityCard from './DeviceSecurityCard';
|
|||
import { filterDevicesBySecurityRecommendation, INACTIVE_DEVICE_AGE_DAYS } from './filter';
|
||||
import {
|
||||
DeviceSecurityVariation,
|
||||
DeviceWithVerification,
|
||||
ExtendedDevice,
|
||||
DevicesDictionary,
|
||||
} from './types';
|
||||
|
||||
interface Props {
|
||||
devices: DevicesDictionary;
|
||||
currentDeviceId: DeviceWithVerification['device_id'];
|
||||
currentDeviceId: ExtendedDevice['device_id'];
|
||||
goToFilteredList: (filter: DeviceSecurityVariation) => void;
|
||||
}
|
||||
|
||||
|
@ -38,7 +38,7 @@ const SecurityRecommendations: React.FC<Props> = ({
|
|||
currentDeviceId,
|
||||
goToFilteredList,
|
||||
}) => {
|
||||
const devicesArray = Object.values<DeviceWithVerification>(devices);
|
||||
const devicesArray = Object.values<ExtendedDevice>(devices);
|
||||
|
||||
const unverifiedDevicesCount = filterDevicesBySecurityRecommendation(
|
||||
devicesArray,
|
||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { DeviceWithVerification, DeviceSecurityVariation } from "./types";
|
||||
import { ExtendedDevice, DeviceSecurityVariation } from "./types";
|
||||
|
||||
type DeviceFilterCondition = (device: DeviceWithVerification) => boolean;
|
||||
type DeviceFilterCondition = (device: ExtendedDevice) => boolean;
|
||||
|
||||
const MS_DAY = 24 * 60 * 60 * 1000;
|
||||
export const INACTIVE_DEVICE_AGE_MS = 7.776e+9; // 90 days
|
||||
|
@ -32,7 +32,7 @@ const filters: Record<DeviceSecurityVariation, DeviceFilterCondition> = {
|
|||
};
|
||||
|
||||
export const filterDevicesBySecurityRecommendation = (
|
||||
devices: DeviceWithVerification[],
|
||||
devices: ExtendedDevice[],
|
||||
securityVariations: DeviceSecurityVariation[],
|
||||
) => {
|
||||
const activeFilters = securityVariations.map(variation => filters[variation]);
|
||||
|
|
|
@ -16,14 +16,17 @@ limitations under the License.
|
|||
|
||||
import { IMyDevice } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { ExtendedDeviceInformation } from "../../../../utils/device/parseUserAgent";
|
||||
|
||||
export type DeviceWithVerification = IMyDevice & { isVerified: boolean | null };
|
||||
export type ExtendedDeviceInfo = {
|
||||
clientName?: string;
|
||||
clientVersion?: string;
|
||||
export type ExtendedDeviceAppInfo = {
|
||||
// eg Element Web
|
||||
appName?: string;
|
||||
appVersion?: string;
|
||||
url?: string;
|
||||
};
|
||||
export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceInfo;
|
||||
export type DevicesDictionary = Record<DeviceWithVerification['device_id'], ExtendedDevice>;
|
||||
export type ExtendedDevice = DeviceWithVerification & ExtendedDeviceAppInfo & ExtendedDeviceInformation;
|
||||
export type DevicesDictionary = Record<ExtendedDevice['device_id'], ExtendedDevice>;
|
||||
|
||||
export enum DeviceSecurityVariation {
|
||||
Verified = 'Verified',
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
MatrixEvent,
|
||||
PUSHER_DEVICE_ID,
|
||||
PUSHER_ENABLED,
|
||||
UNSTABLE_MSC3852_LAST_SEEN_UA,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import { CrossSigningInfo } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
@ -34,8 +35,9 @@ import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifi
|
|||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { getDeviceClientInformation } from "../../../../utils/device/clientInformation";
|
||||
import { DevicesDictionary, DeviceWithVerification, ExtendedDeviceInfo } from "./types";
|
||||
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
||||
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
||||
|
||||
const isDeviceVerified = (
|
||||
matrixClient: MatrixClient,
|
||||
|
@ -63,12 +65,12 @@ const isDeviceVerified = (
|
|||
}
|
||||
};
|
||||
|
||||
const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceInfo => {
|
||||
const parseDeviceExtendedInformation = (matrixClient: MatrixClient, device: IMyDevice): ExtendedDeviceAppInfo => {
|
||||
const { name, version, url } = getDeviceClientInformation(matrixClient, device.device_id);
|
||||
|
||||
return {
|
||||
clientName: name,
|
||||
clientVersion: version,
|
||||
appName: name,
|
||||
appVersion: version,
|
||||
url,
|
||||
};
|
||||
};
|
||||
|
@ -87,6 +89,7 @@ const fetchDevicesWithVerification = async (
|
|||
...device,
|
||||
isVerified: isDeviceVerified(matrixClient, crossSigningInfo, device),
|
||||
...parseDeviceExtendedInformation(matrixClient, device),
|
||||
...parseUserAgent(device[UNSTABLE_MSC3852_LAST_SEEN_UA.name]),
|
||||
},
|
||||
}), {});
|
||||
|
||||
|
@ -104,10 +107,10 @@ export type DevicesState = {
|
|||
currentDeviceId: string;
|
||||
isLoadingDeviceList: boolean;
|
||||
// not provided when current session cannot request verification
|
||||
requestDeviceVerification?: (deviceId: DeviceWithVerification['device_id']) => Promise<VerificationRequest>;
|
||||
requestDeviceVerification?: (deviceId: ExtendedDevice['device_id']) => Promise<VerificationRequest>;
|
||||
refreshDevices: () => Promise<void>;
|
||||
saveDeviceName: (deviceId: DeviceWithVerification['device_id'], deviceName: string) => Promise<void>;
|
||||
setPushNotifications: (deviceId: DeviceWithVerification['device_id'], enabled: boolean) => Promise<void>;
|
||||
saveDeviceName: (deviceId: ExtendedDevice['device_id'], deviceName: string) => Promise<void>;
|
||||
setPushNotifications: (deviceId: ExtendedDevice['device_id'], enabled: boolean) => Promise<void>;
|
||||
error?: OwnDevicesError;
|
||||
supportsMSC3881?: boolean | undefined;
|
||||
};
|
||||
|
@ -189,7 +192,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
const isCurrentDeviceVerified = !!devices[currentDeviceId]?.isVerified;
|
||||
|
||||
const requestDeviceVerification = isCurrentDeviceVerified && userId
|
||||
? async (deviceId: DeviceWithVerification['device_id']) => {
|
||||
? async (deviceId: ExtendedDevice['device_id']) => {
|
||||
return await matrixClient.requestVerification(
|
||||
userId,
|
||||
[deviceId],
|
||||
|
@ -198,7 +201,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
: undefined;
|
||||
|
||||
const saveDeviceName = useCallback(
|
||||
async (deviceId: DeviceWithVerification['device_id'], deviceName: string): Promise<void> => {
|
||||
async (deviceId: ExtendedDevice['device_id'], deviceName: string): Promise<void> => {
|
||||
const device = devices[deviceId];
|
||||
|
||||
// no change
|
||||
|
@ -219,7 +222,7 @@ export const useOwnDevices = (): DevicesState => {
|
|||
}, [matrixClient, devices, refreshDevices]);
|
||||
|
||||
const setPushNotifications = useCallback(
|
||||
async (deviceId: DeviceWithVerification['device_id'], enabled: boolean): Promise<void> => {
|
||||
async (deviceId: ExtendedDevice['device_id'], enabled: boolean): Promise<void> => {
|
||||
try {
|
||||
const pusher = pushers.find(pusher => pusher[PUSHER_DEVICE_ID.name] === deviceId);
|
||||
if (pusher) {
|
||||
|
|
|
@ -31,6 +31,8 @@ import PowerSelector from "../../../elements/PowerSelector";
|
|||
import SettingsFieldset from '../../SettingsFieldset';
|
||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||
import { VoiceBroadcastInfoEventType } from '../../../../../voice-broadcast';
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
|
||||
|
||||
interface IEventShowOpts {
|
||||
isState?: boolean;
|
||||
|
@ -60,6 +62,10 @@ const plEventsToShow: Record<string, IEventShowOpts> = {
|
|||
[EventType.Reaction]: { isState: false, hideForSpace: true },
|
||||
[EventType.RoomRedaction]: { isState: false, hideForSpace: true },
|
||||
|
||||
// MSC3401: Native Group VoIP signaling
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: { isState: true, hideForSpace: true },
|
||||
|
||||
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
|
||||
"im.vector.modular.widgets": { isState: true, hideForSpace: true },
|
||||
[VoiceBroadcastInfoEventType]: { isState: true, hideForSpace: true },
|
||||
|
@ -252,6 +258,11 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
if (SettingsStore.getValue("feature_pinning")) {
|
||||
plEventsToLabels[EventType.RoomPinnedEvents] = _td("Manage pinned events");
|
||||
}
|
||||
// MSC3401: Native Group VoIP signaling
|
||||
if (SettingsStore.getValue("feature_group_calls")) {
|
||||
plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("Start %(brand)s calls");
|
||||
plEventsToLabels[ElementCall.MEMBER_EVENT_TYPE.name] = _td("Join %(brand)s calls");
|
||||
}
|
||||
|
||||
const powerLevelDescriptors: Record<string, IPowerLevelDescriptor> = {
|
||||
"users_default": {
|
||||
|
@ -435,7 +446,8 @@ export default class RolesRoomSettingsTab extends React.Component<IProps> {
|
|||
|
||||
let label = plEventsToLabels[eventType];
|
||||
if (label) {
|
||||
label = _t(label);
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
label = _t(label, { brand });
|
||||
} else {
|
||||
label = _t("Send %(eventType)s events", { eventType });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
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, { useCallback, useMemo, useState } from 'react';
|
||||
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
|
||||
import { _t } from "../../../../../languageHandler";
|
||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||
import SettingsSubsection from "../../shared/SettingsSubsection";
|
||||
import SettingsTab from "../SettingsTab";
|
||||
import { ElementCall } from "../../../../../models/Call";
|
||||
import { useRoomState } from "../../../../../hooks/useRoomState";
|
||||
import SdkConfig, { DEFAULTS } from "../../../../../SdkConfig";
|
||||
|
||||
interface ElementCallSwitchProps {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ roomId }) => {
|
||||
const room = useMemo(() => MatrixClientPeg.get().getRoom(roomId), [roomId]);
|
||||
const isPublic = useMemo(() => room.getJoinRule() === JoinRule.Public, [room]);
|
||||
const [content, events, maySend] = useRoomState(room, useCallback((state) => {
|
||||
const content = state?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||
return [
|
||||
content ?? {},
|
||||
content?.["events"] ?? {},
|
||||
state?.maySendStateEvent(EventType.RoomPowerLevels, MatrixClientPeg.get().getUserId()),
|
||||
];
|
||||
}, []));
|
||||
|
||||
const [elementCallEnabled, setElementCallEnabled] = useState<boolean>(() => {
|
||||
return events[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
|
||||
});
|
||||
|
||||
const onChange = useCallback((enabled: boolean): void => {
|
||||
setElementCallEnabled(enabled);
|
||||
|
||||
if (enabled) {
|
||||
const userLevel = events[EventType.RoomMessage] ?? content.users_default ?? 0;
|
||||
const moderatorLevel = content.kick ?? 50;
|
||||
|
||||
events[ElementCall.CALL_EVENT_TYPE.name] = isPublic ? moderatorLevel : userLevel;
|
||||
events[ElementCall.MEMBER_EVENT_TYPE.name] = userLevel;
|
||||
} else {
|
||||
const adminLevel = events[EventType.RoomPowerLevels] ?? content.state_default ?? 100;
|
||||
|
||||
events[ElementCall.CALL_EVENT_TYPE.name] = adminLevel;
|
||||
events[ElementCall.MEMBER_EVENT_TYPE.name] = adminLevel;
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(roomId, EventType.RoomPowerLevels, {
|
||||
"events": events,
|
||||
...content,
|
||||
});
|
||||
}, [roomId, content, events, isPublic]);
|
||||
|
||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||
|
||||
return <LabelledToggleSwitch
|
||||
data-testid="element-call-switch"
|
||||
label={_t("Enable %(brand)s as an additional calling option in this room", { brand })}
|
||||
caption={_t(
|
||||
"%(brand)s is end-to-end encrypted, " +
|
||||
"but is currently limited to smaller numbers of users.",
|
||||
{ brand },
|
||||
)}
|
||||
value={elementCallEnabled}
|
||||
onChange={onChange}
|
||||
disabled={!maySend}
|
||||
tooltip={_t("You do not have sufficient permissions to change this.")}
|
||||
/>;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
roomId: string;
|
||||
}
|
||||
|
||||
export const VoipRoomSettingsTab: React.FC<Props> = ({ roomId }) => {
|
||||
return <SettingsTab heading={_t("Voice & Video")}>
|
||||
<SettingsSubsection heading={_t("Call type")}>
|
||||
<ElementCallSwitch roomId={roomId} />
|
||||
</SettingsSubsection>
|
||||
</SettingsTab>;
|
||||
};
|
|
@ -214,7 +214,10 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||
{ _t("Share your activity and status with others.") }
|
||||
</span>
|
||||
<SettingsFlag
|
||||
disabled={!this.state.disablingReadReceiptsSupported}
|
||||
disabled={
|
||||
!this.state.disablingReadReceiptsSupported
|
||||
&& SettingsStore.getValue("sendReadReceipts") // Make sure the feature can always be enabled
|
||||
}
|
||||
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
|
||||
name="sendReadReceipts"
|
||||
level={SettingLevel.ACCOUNT}
|
||||
|
|
|
@ -29,7 +29,7 @@ import { useOwnDevices } from '../../devices/useOwnDevices';
|
|||
import { FilteredDeviceList } from '../../devices/FilteredDeviceList';
|
||||
import CurrentDeviceSection from '../../devices/CurrentDeviceSection';
|
||||
import SecurityRecommendations from '../../devices/SecurityRecommendations';
|
||||
import { DeviceSecurityVariation, DeviceWithVerification } from '../../devices/types';
|
||||
import { DeviceSecurityVariation, ExtendedDevice } from '../../devices/types';
|
||||
import { deleteDevicesWithInteractiveAuth } from '../../devices/deleteDevices';
|
||||
import SettingsTab from '../SettingsTab';
|
||||
|
||||
|
@ -38,10 +38,10 @@ const useSignOut = (
|
|||
onSignoutResolvedCallback: () => Promise<void>,
|
||||
): {
|
||||
onSignOutCurrentDevice: () => void;
|
||||
onSignOutOtherDevices: (deviceIds: DeviceWithVerification['device_id'][]) => Promise<void>;
|
||||
signingOutDeviceIds: DeviceWithVerification['device_id'][];
|
||||
onSignOutOtherDevices: (deviceIds: ExtendedDevice['device_id'][]) => Promise<void>;
|
||||
signingOutDeviceIds: ExtendedDevice['device_id'][];
|
||||
} => {
|
||||
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
|
||||
const [signingOutDeviceIds, setSigningOutDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
|
||||
const onSignOutCurrentDevice = () => {
|
||||
Modal.createDialog(
|
||||
|
@ -53,7 +53,7 @@ const useSignOut = (
|
|||
);
|
||||
};
|
||||
|
||||
const onSignOutOtherDevices = async (deviceIds: DeviceWithVerification['device_id'][]) => {
|
||||
const onSignOutOtherDevices = async (deviceIds: ExtendedDevice['device_id'][]) => {
|
||||
if (!deviceIds.length) {
|
||||
return;
|
||||
}
|
||||
|
@ -96,8 +96,8 @@ const SessionManagerTab: React.FC = () => {
|
|||
supportsMSC3881,
|
||||
} = useOwnDevices();
|
||||
const [filter, setFilter] = useState<DeviceSecurityVariation>();
|
||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<DeviceWithVerification['device_id'][]>([]);
|
||||
const [expandedDeviceIds, setExpandedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const [selectedDeviceIds, setSelectedDeviceIds] = useState<ExtendedDevice['device_id'][]>([]);
|
||||
const filteredDeviceListRef = useRef<HTMLDivElement>(null);
|
||||
const scrollIntoViewTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
|
@ -105,7 +105,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
const userId = matrixClient.getUserId();
|
||||
const currentUserMember = userId && matrixClient.getUser(userId) || undefined;
|
||||
|
||||
const onDeviceExpandToggle = (deviceId: DeviceWithVerification['device_id']): void => {
|
||||
const onDeviceExpandToggle = (deviceId: ExtendedDevice['device_id']): void => {
|
||||
if (expandedDeviceIds.includes(deviceId)) {
|
||||
setExpandedDeviceIds(expandedDeviceIds.filter(id => id !== deviceId));
|
||||
} else {
|
||||
|
@ -136,7 +136,7 @@ const SessionManagerTab: React.FC = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const onTriggerDeviceVerification = useCallback((deviceId: DeviceWithVerification['device_id']) => {
|
||||
const onTriggerDeviceVerification = useCallback((deviceId: ExtendedDevice['device_id']) => {
|
||||
if (!requestDeviceVerification) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import { defer, IDeferred } from "matrix-js-sdk/src/utils";
|
|||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { ConnectionState } from "../../../models/Call";
|
||||
import { Call, CallEvent, ElementCall, isConnected } from "../../../models/Call";
|
||||
import { useCall, useConnectionState, useParticipants } from "../../../hooks/useCall";
|
||||
import { useCall, useConnectionState, useJoinCallButtonDisabledTooltip, useParticipants } from "../../../hooks/useCall";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
@ -35,7 +35,7 @@ import IconizedContextMenu, {
|
|||
} from "../context_menus/IconizedContextMenu";
|
||||
import { aboveLeftOf, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
|
||||
import { Alignment } from "../elements/Tooltip";
|
||||
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||
import FacePile from "../elements/FacePile";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
|
@ -110,10 +110,11 @@ const MAX_FACES = 8;
|
|||
interface LobbyProps {
|
||||
room: Room;
|
||||
connect: () => Promise<void>;
|
||||
joinCallButtonDisabledTooltip?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
|
||||
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, connect, children }) => {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
|
@ -233,14 +234,15 @@ export const Lobby: FC<LobbyProps> = ({ room, connect, children }) => {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<AccessibleButton
|
||||
<AccessibleTooltipButton
|
||||
className="mx_CallView_connectButton"
|
||||
kind="primary"
|
||||
disabled={connecting}
|
||||
disabled={connecting || Boolean(joinCallButtonDisabledTooltip)}
|
||||
onClick={onConnectClick}
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleButton>
|
||||
title={_t("Join")}
|
||||
label={_t("Join")}
|
||||
tooltip={connecting ? _t("Connecting") : joinCallButtonDisabledTooltip}
|
||||
/>
|
||||
</div>;
|
||||
};
|
||||
|
||||
|
@ -321,6 +323,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
|
|||
const cli = useContext(MatrixClientContext);
|
||||
const connected = isConnected(useConnectionState(call));
|
||||
const participants = useParticipants(call);
|
||||
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
|
||||
const connect = useCallback(async () => {
|
||||
// Disconnect from any other active calls first, since we don't yet support holding
|
||||
|
@ -344,7 +347,13 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call }) => {
|
|||
</div>;
|
||||
}
|
||||
|
||||
lobby = <Lobby room={room} connect={connect}>{ facePile }</Lobby>;
|
||||
lobby = <Lobby
|
||||
room={room}
|
||||
connect={connect}
|
||||
joinCallButtonDisabledTooltip={joinCallButtonDisabledTooltip}
|
||||
>
|
||||
{ facePile }
|
||||
</Lobby>;
|
||||
}
|
||||
|
||||
return <div className="mx_CallView">
|
||||
|
|
|
@ -32,7 +32,7 @@ const LegacyCallViewHeaderControls: React.FC<LegacyCallControlsProps> = ({ onExp
|
|||
{ onMaximize && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_fullscreen"
|
||||
onClick={onMaximize}
|
||||
title={_t("Fill Screen")}
|
||||
title={_t("Fill screen")}
|
||||
/> }
|
||||
{ onPin && <AccessibleTooltipButton
|
||||
className="mx_LegacyCallViewHeader_button mx_LegacyCallViewHeader_button_pin"
|
||||
|
|
|
@ -201,7 +201,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<aside
|
||||
className={this.props.className}
|
||||
style={style}
|
||||
ref={this.callViewWrapper}
|
||||
|
@ -211,7 +211,7 @@ export default class PictureInPictureDragger extends React.Component<IProps> {
|
|||
onStartMoving: this.onStartMoving,
|
||||
onResize: this.onResize,
|
||||
}) }
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,6 @@ import LegacyCallView from "./LegacyCallView";
|
|||
import { RoomViewStore } from '../../../stores/RoomViewStore';
|
||||
import LegacyCallHandler, { LegacyCallHandlerEvent } from '../../../LegacyCallHandler';
|
||||
import PersistentApp from "../elements/PersistentApp";
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||
import PictureInPictureDragger from './PictureInPictureDragger';
|
||||
import dis from '../../../dispatcher/dispatcher';
|
||||
|
@ -35,6 +34,7 @@ import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/Activ
|
|||
import WidgetStore, { IApp } from "../../../stores/WidgetStore";
|
||||
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||
import { UPDATE_EVENT } from '../../../stores/AsyncStore';
|
||||
import { CallStore } from "../../../stores/CallStore";
|
||||
|
||||
const SHOW_CALL_IN_STATES = [
|
||||
CallState.Connected,
|
||||
|
@ -116,7 +116,6 @@ function getPrimarySecondaryCallsForPip(roomId: string): [MatrixCall, MatrixCall
|
|||
*/
|
||||
|
||||
export default class PipView extends React.Component<IProps, IState> {
|
||||
private settingsWatcherRef: string;
|
||||
private movePersistedElement = createRef<() => void>();
|
||||
|
||||
constructor(props: IProps) {
|
||||
|
@ -157,7 +156,6 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
|
||||
MatrixClientPeg.get().removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
|
||||
RoomViewStore.instance.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
SettingsStore.unwatchSetting(this.settingsWatcherRef);
|
||||
const room = MatrixClientPeg.get().getRoom(this.state.viewedRoomId);
|
||||
if (room) {
|
||||
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
|
||||
|
@ -278,6 +276,14 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
});
|
||||
};
|
||||
|
||||
private onViewCall = (): void =>
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.persistentRoomId,
|
||||
view_call: true,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
|
||||
// Accepts a persistentWidgetId to be able to skip awaiting the setState for persistentWidgetId
|
||||
public updateShowWidgetInPip(
|
||||
persistentWidgetId = this.state.persistentWidgetId,
|
||||
|
@ -323,18 +329,19 @@ export default class PipView extends React.Component<IProps, IState> {
|
|||
mx_LegacyCallView_large: !pipMode,
|
||||
});
|
||||
const roomId = this.state.persistentRoomId;
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId);
|
||||
const roomForWidget = MatrixClientPeg.get().getRoom(roomId)!;
|
||||
const viewingCallRoom = this.state.viewedRoomId === roomId;
|
||||
const isCall = CallStore.instance.getActiveCall(roomId) !== null;
|
||||
|
||||
pipContent = ({ onStartMoving, _onResize }) =>
|
||||
pipContent = ({ onStartMoving }) =>
|
||||
<div className={pipViewClasses}>
|
||||
<LegacyCallViewHeader
|
||||
onPipMouseDown={(event) => { onStartMoving(event); this.onStartMoving.bind(this)(); }}
|
||||
pipMode={pipMode}
|
||||
callRooms={[roomForWidget]}
|
||||
onExpand={!viewingCallRoom && this.onExpand}
|
||||
onPin={viewingCallRoom && this.onPin}
|
||||
onMaximize={viewingCallRoom && this.onMaximize}
|
||||
onExpand={!isCall && !viewingCallRoom ? this.onExpand : undefined}
|
||||
onPin={!isCall && viewingCallRoom ? this.onPin : undefined}
|
||||
onMaximize={isCall ? this.onViewCall : viewingCallRoom ? this.onMaximize : undefined}
|
||||
/>
|
||||
<PersistentApp
|
||||
persistentWidgetId={this.state.persistentWidgetId}
|
||||
|
|
|
@ -65,6 +65,7 @@ const RoomContext = createContext<IRoomState>({
|
|||
threadId: undefined,
|
||||
liveTimeline: undefined,
|
||||
narrow: false,
|
||||
activeCall: null,
|
||||
});
|
||||
RoomContext.displayName = "RoomContext";
|
||||
export default RoomContext;
|
||||
|
|
|
@ -46,6 +46,7 @@ import { findDMForUser } from "./utils/dm/findDMForUser";
|
|||
import { privateShouldBeEncrypted } from "./utils/rooms";
|
||||
import { waitForMember } from "./utils/membership";
|
||||
import { PreferredRoomVersions } from "./utils/PreferredRoomVersions";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
|
||||
// we define a number of interfaces which take their names from the js-sdk
|
||||
/* eslint-disable camelcase */
|
||||
|
@ -168,6 +169,16 @@ export default async function createRoom(opts: IOpts): Promise<string | null> {
|
|||
},
|
||||
};
|
||||
}
|
||||
} else if (SettingsStore.getValue("feature_group_calls")) {
|
||||
createOpts.power_level_content_override = {
|
||||
events: {
|
||||
...DEFAULT_EVENT_POWER_LEVELS,
|
||||
// Element Call should be disabled by default
|
||||
[ElementCall.MEMBER_EVENT_TYPE.name]: 100,
|
||||
// Make sure only admins can enable it
|
||||
[ElementCall.CALL_EVENT_TYPE.name]: 100,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// By default, view the room after creating it
|
||||
|
|
|
@ -1,45 +0,0 @@
|
|||
/*
|
||||
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 * as React from "react";
|
||||
|
||||
import Modal from "./Modal";
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import { _t } from "./languageHandler";
|
||||
import SdkConfig, { DEFAULTS } from "./SdkConfig";
|
||||
|
||||
export function showGroupReplacedWithSpacesDialog(groupId: string) {
|
||||
const learnMoreUrl = SdkConfig.get().spaces_learn_more_url ?? DEFAULTS.spaces_learn_more_url;
|
||||
Modal.createDialog(QuestionDialog, {
|
||||
title: _t("That link is no longer supported"),
|
||||
description: <>
|
||||
<p>
|
||||
{ _t(
|
||||
"You're trying to access a community link (%(groupId)s).<br/>" +
|
||||
"Communities are no longer supported and have been replaced by spaces.<br2/>" +
|
||||
"<a>Learn more about spaces here.</a>",
|
||||
{ groupId },
|
||||
{
|
||||
br: () => <br />,
|
||||
br2: () => <br />,
|
||||
a: (sub) => <a href={learnMoreUrl} rel="noreferrer noopener" target="_blank">{ sub }</a>,
|
||||
},
|
||||
) }
|
||||
</p>
|
||||
</>,
|
||||
hasCancelButton: false,
|
||||
});
|
||||
}
|
|
@ -17,14 +17,16 @@ limitations under the License.
|
|||
import { useState, useCallback } from "react";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { Call, ConnectionState } from "../models/Call";
|
||||
import { Call, ConnectionState, ElementCall, Layout } from "../models/Call";
|
||||
import { useTypedEventEmitterState } from "./useEventEmitter";
|
||||
import { CallEvent } from "../models/Call";
|
||||
import { CallStore, CallStoreEvent } from "../stores/CallStore";
|
||||
import { useEventEmitter } from "./useEventEmitter";
|
||||
import SdkConfig, { DEFAULTS } from "../SdkConfig";
|
||||
import { _t } from "../languageHandler";
|
||||
|
||||
export const useCall = (roomId: string): Call | null => {
|
||||
const [call, setCall] = useState(() => CallStore.instance.get(roomId));
|
||||
const [call, setCall] = useState(() => CallStore.instance.getCall(roomId));
|
||||
useEventEmitter(CallStore.instance, CallStoreEvent.Call, (call: Call | null, forRoomId: string) => {
|
||||
if (forRoomId === roomId) setCall(call);
|
||||
});
|
||||
|
@ -44,3 +46,28 @@ export const useParticipants = (call: Call): Set<RoomMember> =>
|
|||
CallEvent.Participants,
|
||||
useCallback(state => state ?? call.participants, [call]),
|
||||
);
|
||||
|
||||
export const useFull = (call: Call): boolean => {
|
||||
const participants = useParticipants(call);
|
||||
|
||||
return (
|
||||
participants.size
|
||||
>= (SdkConfig.get("element_call").participant_limit ?? DEFAULTS.element_call.participant_limit)
|
||||
);
|
||||
};
|
||||
|
||||
export const useJoinCallButtonDisabledTooltip = (call: Call): string | null => {
|
||||
const isFull = useFull(call);
|
||||
const state = useConnectionState(call);
|
||||
|
||||
if (state === ConnectionState.Connecting) return _t("Connecting");
|
||||
if (isFull) return _t("Sorry — this call is currently full");
|
||||
return null;
|
||||
};
|
||||
|
||||
export const useLayout = (call: ElementCall): Layout =>
|
||||
useTypedEventEmitterState(
|
||||
call,
|
||||
CallEvent.Layout,
|
||||
useCallback(state => state ?? call.layout, [call]),
|
||||
);
|
||||
|
|
|
@ -52,8 +52,6 @@
|
|||
"%(value)sh": "%(value)sh",
|
||||
"%(value)sm": "%(value)sm",
|
||||
"%(value)ss": "%(value)ss",
|
||||
"That link is no longer supported": "That link is no longer supported",
|
||||
"You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>": "You're trying to access a community link (%(groupId)s).<br/>Communities are no longer supported and have been replaced by spaces.<br2/><a>Learn more about spaces here.</a>",
|
||||
"Identity server has no terms of service": "Identity server has no terms of service",
|
||||
"This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.": "This action requires accessing the default identity server <server /> to validate an email address or phone number, but the server does not have any terms of service.",
|
||||
"Only continue if you trust the owner of the server.": "Only continue if you trust the owner of the server.",
|
||||
|
@ -472,6 +470,8 @@
|
|||
"Converts the DM to a room": "Converts the DM to a room",
|
||||
"Displays action": "Displays action",
|
||||
"Someone": "Someone",
|
||||
"Video call started in %(roomName)s.": "Video call started in %(roomName)s.",
|
||||
"Video call started in %(roomName)s. (not supported by this browser)": "Video call started in %(roomName)s. (not supported by this browser)",
|
||||
"%(senderName)s placed a voice call.": "%(senderName)s placed a voice call.",
|
||||
"%(senderName)s placed a voice call. (not supported by this browser)": "%(senderName)s placed a voice call. (not supported by this browser)",
|
||||
"%(senderName)s placed a video call.": "%(senderName)s placed a video call.",
|
||||
|
@ -797,6 +797,11 @@
|
|||
"Don't miss a reply": "Don't miss a reply",
|
||||
"Notifications": "Notifications",
|
||||
"Enable desktop notifications": "Enable desktop notifications",
|
||||
"Join": "Join",
|
||||
"Unknown room": "Unknown room",
|
||||
"Video call started": "Video call started",
|
||||
"Video": "Video",
|
||||
"Close": "Close",
|
||||
"Unknown caller": "Unknown caller",
|
||||
"Voice call": "Voice call",
|
||||
"Video call": "Video call",
|
||||
|
@ -1010,6 +1015,8 @@
|
|||
"When rooms are upgraded": "When rooms are upgraded",
|
||||
"My Ban List": "My Ban List",
|
||||
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
|
||||
"Connecting": "Connecting",
|
||||
"Sorry — this call is currently full": "Sorry — this call is currently full",
|
||||
"Create account": "Create account",
|
||||
"You made it!": "You made it!",
|
||||
"Find and invite your friends": "Find and invite your friends",
|
||||
|
@ -1054,7 +1061,6 @@
|
|||
"Video devices": "Video devices",
|
||||
"Turn off camera": "Turn off camera",
|
||||
"Turn on camera": "Turn on camera",
|
||||
"Join": "Join",
|
||||
"%(count)s people joined|other": "%(count)s people joined",
|
||||
"%(count)s people joined|one": "%(count)s person joined",
|
||||
"Dial": "Dial",
|
||||
|
@ -1067,7 +1073,6 @@
|
|||
"You held the call <a>Switch</a>": "You held the call <a>Switch</a>",
|
||||
"You held the call <a>Resume</a>": "You held the call <a>Resume</a>",
|
||||
"%(peerName)s held the call": "%(peerName)s held the call",
|
||||
"Connecting": "Connecting",
|
||||
"Dialpad": "Dialpad",
|
||||
"Mute the microphone": "Mute the microphone",
|
||||
"Unmute the microphone": "Unmute the microphone",
|
||||
|
@ -1079,7 +1084,7 @@
|
|||
"Show sidebar": "Show sidebar",
|
||||
"More": "More",
|
||||
"Hangup": "Hangup",
|
||||
"Fill Screen": "Fill Screen",
|
||||
"Fill screen": "Fill screen",
|
||||
"Pin": "Pin",
|
||||
"Return to call": "Return to call",
|
||||
"%(name)s on hold": "%(name)s on hold",
|
||||
|
@ -1522,7 +1527,6 @@
|
|||
"Ban list rules - %(roomName)s": "Ban list rules - %(roomName)s",
|
||||
"Server rules": "Server rules",
|
||||
"User rules": "User rules",
|
||||
"Close": "Close",
|
||||
"You have not ignored anyone.": "You have not ignored anyone.",
|
||||
"You are currently ignoring:": "You are currently ignoring:",
|
||||
"You are not subscribed to any lists": "You are not subscribed to any lists",
|
||||
|
@ -1648,6 +1652,8 @@
|
|||
"Modify widgets": "Modify widgets",
|
||||
"Voice broadcasts": "Voice broadcasts",
|
||||
"Manage pinned events": "Manage pinned events",
|
||||
"Start %(brand)s calls": "Start %(brand)s calls",
|
||||
"Join %(brand)s calls": "Join %(brand)s calls",
|
||||
"Default role": "Default role",
|
||||
"Send messages": "Send messages",
|
||||
"Invite users": "Invite users",
|
||||
|
@ -1687,6 +1693,10 @@
|
|||
"Security & Privacy": "Security & Privacy",
|
||||
"Once enabled, encryption cannot be disabled.": "Once enabled, encryption cannot be disabled.",
|
||||
"Encrypted": "Encrypted",
|
||||
"Enable %(brand)s as an additional calling option in this room": "Enable %(brand)s as an additional calling option in this room",
|
||||
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.",
|
||||
"You do not have sufficient permissions to change this.": "You do not have sufficient permissions to change this.",
|
||||
"Call type": "Call type",
|
||||
"Unable to revoke sharing for email address": "Unable to revoke sharing for email address",
|
||||
"Unable to share email address": "Unable to share email address",
|
||||
"Your email address hasn't been verified yet": "Your email address hasn't been verified yet",
|
||||
|
@ -1717,11 +1727,14 @@
|
|||
"Rename session": "Rename session",
|
||||
"Please be aware that session names are also visible to people you communicate with": "Please be aware that session names are also visible to people you communicate with",
|
||||
"Session ID": "Session ID",
|
||||
"Client": "Client",
|
||||
"Last activity": "Last activity",
|
||||
"Application": "Application",
|
||||
"Version": "Version",
|
||||
"URL": "URL",
|
||||
"Device": "Device",
|
||||
"Model": "Model",
|
||||
"Operating system": "Operating system",
|
||||
"IP address": "IP address",
|
||||
"Session details": "Session details",
|
||||
"Toggle push notifications on this session.": "Toggle push notifications on this session.",
|
||||
|
@ -1732,7 +1745,10 @@
|
|||
"Inactive for %(inactiveAgeDays)s+ days": "Inactive for %(inactiveAgeDays)s+ days",
|
||||
"Verified": "Verified",
|
||||
"Unverified": "Unverified",
|
||||
"Unknown device type": "Unknown device type",
|
||||
"Desktop session": "Desktop session",
|
||||
"Mobile session": "Mobile session",
|
||||
"Web session": "Web session",
|
||||
"Unknown session type": "Unknown session type",
|
||||
"Verified session": "Verified session",
|
||||
"This session is ready for secure messaging.": "This session is ready for secure messaging.",
|
||||
"Unverified session": "Unverified session",
|
||||
|
@ -1884,16 +1900,21 @@
|
|||
"Recently visited rooms": "Recently visited rooms",
|
||||
"No recently visited rooms": "No recently visited rooms",
|
||||
"Video call (Jitsi)": "Video call (Jitsi)",
|
||||
"Video call (Element Call)": "Video call (Element Call)",
|
||||
"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",
|
||||
"Layout type": "Layout type",
|
||||
"Forget room": "Forget room",
|
||||
"Hide Widgets": "Hide Widgets",
|
||||
"Show Widgets": "Show Widgets",
|
||||
"Search": "Search",
|
||||
"Invite": "Invite",
|
||||
"Close call": "Close call",
|
||||
"View chat timeline": "View chat timeline",
|
||||
"Room options": "Room options",
|
||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||
|
@ -2005,7 +2026,6 @@
|
|||
"%(count)s unread messages.|other": "%(count)s unread messages.",
|
||||
"%(count)s unread messages.|one": "1 unread message.",
|
||||
"Unread messages.": "Unread messages.",
|
||||
"Video": "Video",
|
||||
"Joining…": "Joining…",
|
||||
"Joined": "Joined",
|
||||
"Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.": "Upgrading this room will shut down the current instance of the room and create an upgraded room with the same name.",
|
||||
|
@ -2090,7 +2110,7 @@
|
|||
"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",
|
||||
"Chat": "Chat",
|
||||
"Room Info": "Room Info",
|
||||
"Room info": "Room info",
|
||||
"You can only pin up to %(count)s widgets|other": "You can only pin up to %(count)s widgets",
|
||||
"Maximise": "Maximise",
|
||||
"Unpin this widget to view it in this panel": "Unpin this widget to view it in this panel",
|
||||
|
|
|
@ -30,13 +30,11 @@ import dis from './dispatcher/dispatcher';
|
|||
import { Action } from './dispatcher/actions';
|
||||
import { ViewUserPayload } from './dispatcher/payloads/ViewUserPayload';
|
||||
import { ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
|
||||
import { showGroupReplacedWithSpacesDialog } from "./group_helpers";
|
||||
|
||||
export enum Type {
|
||||
URL = "url",
|
||||
UserId = "userid",
|
||||
RoomAlias = "roomalias",
|
||||
GroupId = "groupid",
|
||||
}
|
||||
|
||||
// Linkify stuff doesn't type scanner/parser/utils properly :/
|
||||
|
@ -115,11 +113,6 @@ function onUserClick(event: MouseEvent, userId: string) {
|
|||
});
|
||||
}
|
||||
|
||||
function onGroupClick(event: MouseEvent, groupId: string) {
|
||||
event.preventDefault();
|
||||
showGroupReplacedWithSpacesDialog(groupId);
|
||||
}
|
||||
|
||||
function onAliasClick(event: MouseEvent, roomAlias: string) {
|
||||
event.preventDefault();
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
|
@ -192,15 +185,6 @@ export const options = {
|
|||
onAliasClick(e, alias);
|
||||
},
|
||||
};
|
||||
|
||||
case Type.GroupId:
|
||||
return {
|
||||
// @ts-ignore see https://linkify.js.org/docs/options.html
|
||||
click: function(e: MouseEvent) {
|
||||
const groupId = parsePermalink(href).groupId;
|
||||
onGroupClick(e, groupId);
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -208,7 +192,6 @@ export const options = {
|
|||
switch (type) {
|
||||
case Type.RoomAlias:
|
||||
case Type.UserId:
|
||||
case Type.GroupId:
|
||||
default: {
|
||||
return tryTransformEntityToPermalink(href);
|
||||
}
|
||||
|
@ -255,17 +238,6 @@ registerPlugin(Type.RoomAlias, ({ scanner, parser, utils }) => {
|
|||
});
|
||||
});
|
||||
|
||||
registerPlugin(Type.GroupId, ({ scanner, parser, utils }) => {
|
||||
const token = scanner.tokens.PLUS as '+';
|
||||
matrixOpaqueIdLinkifyParser({
|
||||
scanner,
|
||||
parser,
|
||||
utils,
|
||||
token,
|
||||
name: Type.GroupId,
|
||||
});
|
||||
});
|
||||
|
||||
registerPlugin(Type.UserId, ({ scanner, parser, utils }) => {
|
||||
const token = scanner.tokens.AT as '@';
|
||||
matrixOpaqueIdLinkifyParser({
|
||||
|
|
|
@ -31,7 +31,7 @@ import type { Room } from "matrix-js-sdk/src/models/room";
|
|||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import type { IApp } from "../stores/WidgetStore";
|
||||
import SdkConfig from "../SdkConfig";
|
||||
import SdkConfig, { DEFAULTS } from "../SdkConfig";
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../MediaDeviceHandler";
|
||||
import { timeout } from "../utils/promise";
|
||||
|
@ -71,15 +71,22 @@ export enum ConnectionState {
|
|||
export const isConnected = (state: ConnectionState): boolean =>
|
||||
state === ConnectionState.Connected || state === ConnectionState.Disconnecting;
|
||||
|
||||
export enum Layout {
|
||||
Tile = "tile",
|
||||
Spotlight = "spotlight",
|
||||
}
|
||||
|
||||
export enum CallEvent {
|
||||
ConnectionState = "connection_state",
|
||||
Participants = "participants",
|
||||
Layout = "layout",
|
||||
Destroy = "destroy",
|
||||
}
|
||||
|
||||
interface CallEventHandlerMap {
|
||||
[CallEvent.ConnectionState]: (state: ConnectionState, prevState: ConnectionState) => void;
|
||||
[CallEvent.Participants]: (participants: Set<RoomMember>, prevParticipants: Set<RoomMember>) => void;
|
||||
[CallEvent.Layout]: (layout: Layout) => void;
|
||||
[CallEvent.Destroy]: () => void;
|
||||
}
|
||||
|
||||
|
@ -110,7 +117,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||
return this.widget.roomId;
|
||||
}
|
||||
|
||||
private _connectionState: ConnectionState = ConnectionState.Disconnected;
|
||||
private _connectionState = ConnectionState.Disconnected;
|
||||
public get connectionState(): ConnectionState {
|
||||
return this._connectionState;
|
||||
}
|
||||
|
@ -604,9 +611,18 @@ export class ElementCall extends Call {
|
|||
private participantsExpirationTimer: number | null = null;
|
||||
private terminationTimer: number | null = null;
|
||||
|
||||
private _layout = Layout.Tile;
|
||||
public get layout(): Layout {
|
||||
return this._layout;
|
||||
}
|
||||
protected set layout(value: Layout) {
|
||||
this._layout = value;
|
||||
this.emit(CallEvent.Layout, value);
|
||||
}
|
||||
|
||||
private constructor(public readonly groupCall: MatrixEvent, client: MatrixClient) {
|
||||
// Splice together the Element Call URL for this call
|
||||
const url = new URL(SdkConfig.get("element_call").url);
|
||||
const url = new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url);
|
||||
url.pathname = "/room";
|
||||
const params = new URLSearchParams({
|
||||
embed: "",
|
||||
|
@ -779,6 +795,8 @@ export class ElementCall extends Call {
|
|||
}
|
||||
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
}
|
||||
|
||||
protected async performDisconnection(): Promise<void> {
|
||||
|
@ -791,6 +809,8 @@ export class ElementCall extends Call {
|
|||
|
||||
public setDisconnected() {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
super.setDisconnected();
|
||||
}
|
||||
|
||||
|
@ -812,6 +832,18 @@ export class ElementCall extends Call {
|
|||
super.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the call's layout.
|
||||
* @param layout The layout to switch to.
|
||||
*/
|
||||
public async setLayout(layout: Layout): Promise<void> {
|
||||
const action = layout === Layout.Tile
|
||||
? ElementWidgetActions.TileLayout
|
||||
: ElementWidgetActions.SpotlightLayout;
|
||||
|
||||
await this.messaging!.transport.send(action, {});
|
||||
}
|
||||
|
||||
private get mayTerminate(): boolean {
|
||||
return this.groupCall.getContent()["m.intent"] !== "m.room"
|
||||
&& this.room.currentState.mayClientSendStateEvent(ElementCall.CALL_EVENT_TYPE.name, this.client);
|
||||
|
@ -869,4 +901,16 @@ export class ElementCall extends Call {
|
|||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
};
|
||||
|
||||
private onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Tile;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>) => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Spotlight;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
|||
await Promise.all([
|
||||
...uncleanlyDisconnectedRoomIds.map(async uncleanlyDisconnectedRoomId => {
|
||||
logger.log(`Cleaning up call state for room ${uncleanlyDisconnectedRoomId}`);
|
||||
await this.get(uncleanlyDisconnectedRoomId)?.clean();
|
||||
await this.getCall(uncleanlyDisconnectedRoomId)?.clean();
|
||||
}),
|
||||
SettingsStore.setValue("activeCallRoomIds", null, SettingLevel.DEVICE, []),
|
||||
]);
|
||||
|
@ -152,18 +152,18 @@ export class CallStore extends AsyncStoreWithClient<{}> {
|
|||
* @param {string} roomId The room's ID.
|
||||
* @returns {Call | null} The call.
|
||||
*/
|
||||
public get(roomId: string): Call | null {
|
||||
public getCall(roomId: string): Call | null {
|
||||
return this.calls.get(roomId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given room has an active call.
|
||||
* Gets the active call associated with the given room, if any.
|
||||
* @param roomId The room's ID.
|
||||
* @returns Whether the given room has an active call.
|
||||
* @returns The active call.
|
||||
*/
|
||||
public hasActiveCall(roomId: string): boolean {
|
||||
const call = this.get(roomId);
|
||||
return call !== null && this.activeCalls.has(call);
|
||||
public getActiveCall(roomId: string): Call | null {
|
||||
const call = this.getCall(roomId);
|
||||
return call !== null && this.activeCalls.has(call) ? call : null;
|
||||
}
|
||||
|
||||
private onRoom = (room: Room) => this.updateRoom(room);
|
||||
|
|
|
@ -365,7 +365,7 @@ export class RoomViewStore extends EventEmitter {
|
|||
viewingCall: payload.view_call ?? (
|
||||
payload.room_id === this.state.roomId
|
||||
? this.state.viewingCall
|
||||
: CallStore.instance.hasActiveCall(payload.room_id)
|
||||
: CallStore.instance.getActiveCall(payload.room_id) !== null
|
||||
),
|
||||
};
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||
import { Direction } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
|
||||
import { iterableDiff, iterableIntersection } from "../../utils/iterables";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import Modal from "../../Modal";
|
||||
|
@ -104,7 +104,10 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
// Auto-approve the legacy visibility capability. We send it regardless of capability.
|
||||
// Widgets don't technically need to request this capability, but Scalar still does.
|
||||
this.allowedCapabilities.add("visibility");
|
||||
} else if (virtual && new URL(SdkConfig.get("element_call").url).origin === this.forWidget.origin) {
|
||||
} else if (
|
||||
virtual
|
||||
&& new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url).origin === this.forWidget.origin
|
||||
) {
|
||||
// This is a trusted Element Call widget that we control
|
||||
this.allowedCapabilities.add(MatrixCapabilities.AlwaysOnScreen);
|
||||
this.allowedCapabilities.add(MatrixCapabilities.MSC3846TurnServers);
|
||||
|
@ -434,7 +437,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
}
|
||||
|
||||
const {
|
||||
originalEvent,
|
||||
events,
|
||||
nextBatch,
|
||||
prevBatch,
|
||||
|
@ -451,7 +453,6 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
|||
});
|
||||
|
||||
return {
|
||||
originalEvent: originalEvent?.getEffectiveEvent(),
|
||||
chunk: events.map(e => e.getEffectiveEvent()),
|
||||
nextBatch,
|
||||
prevBatch,
|
||||
|
|
79
src/theme.ts
|
@ -237,13 +237,13 @@ export async function setTheme(theme?: string): Promise<void> {
|
|||
|
||||
// look for the stylesheet elements.
|
||||
// styleElements is a map from style name to HTMLLinkElement.
|
||||
const styleElements = Object.create(null);
|
||||
const themes = Array.from(document.querySelectorAll('[data-mx-theme]'));
|
||||
const styleElements = new Map<string, HTMLLinkElement>();
|
||||
const themes = Array.from(document.querySelectorAll<HTMLLinkElement>('[data-mx-theme]'));
|
||||
themes.forEach(theme => {
|
||||
styleElements[theme.attributes['data-mx-theme'].value.toLowerCase()] = theme;
|
||||
styleElements.set(theme.attributes['data-mx-theme'].value.toLowerCase(), theme);
|
||||
});
|
||||
|
||||
if (!(stylesheetName in styleElements)) {
|
||||
if (!styleElements.has(stylesheetName)) {
|
||||
throw new Error("Unknown theme " + stylesheetName);
|
||||
}
|
||||
|
||||
|
@ -258,17 +258,18 @@ export async function setTheme(theme?: string): Promise<void> {
|
|||
// having them interact badly... but this causes a flash of unstyled app
|
||||
// which is even uglier. So we don't.
|
||||
|
||||
styleElements[stylesheetName].disabled = false;
|
||||
const styleSheet = styleElements.get(stylesheetName);
|
||||
styleSheet.disabled = false;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(((resolve, reject) => {
|
||||
const switchTheme = function() {
|
||||
// we re-enable our theme here just in case we raced with another
|
||||
// theme set request as per https://github.com/vector-im/element-web/issues/5601.
|
||||
// We could alternatively lock or similar to stop the race, but
|
||||
// this is probably good enough for now.
|
||||
styleElements[stylesheetName].disabled = false;
|
||||
Object.values(styleElements).forEach((a: HTMLStyleElement) => {
|
||||
if (a == styleElements[stylesheetName]) return;
|
||||
styleSheet.disabled = false;
|
||||
styleElements.forEach(a => {
|
||||
if (a == styleSheet) return;
|
||||
a.disabled = true;
|
||||
});
|
||||
const bodyStyles = global.getComputedStyle(document.body);
|
||||
|
@ -279,26 +280,50 @@ export async function setTheme(theme?: string): Promise<void> {
|
|||
resolve();
|
||||
};
|
||||
|
||||
// turns out that Firefox preloads the CSS for link elements with
|
||||
// the disabled attribute, but Chrome doesn't.
|
||||
const isStyleSheetLoaded = () => Boolean(
|
||||
[...document.styleSheets]
|
||||
.find(_styleSheet => _styleSheet?.href === styleSheet.href),
|
||||
);
|
||||
|
||||
let cssLoaded = false;
|
||||
|
||||
styleElements[stylesheetName].onload = () => {
|
||||
switchTheme();
|
||||
};
|
||||
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
const ss = document.styleSheets[i];
|
||||
if (ss && ss.href === styleElements[stylesheetName].href) {
|
||||
cssLoaded = true;
|
||||
break;
|
||||
function waitForStyleSheetLoading() {
|
||||
// turns out that Firefox preloads the CSS for link elements with
|
||||
// the disabled attribute, but Chrome doesn't.
|
||||
if (isStyleSheetLoaded()) {
|
||||
switchTheme();
|
||||
return;
|
||||
}
|
||||
|
||||
let counter = 0;
|
||||
|
||||
// In case of theme toggling (white => black => white)
|
||||
// Chrome doesn't fire the `load` event when the white theme is selected the second times
|
||||
const intervalId = setInterval(() => {
|
||||
if (isStyleSheetLoaded()) {
|
||||
clearInterval(intervalId);
|
||||
styleSheet.onload = undefined;
|
||||
styleSheet.onerror = undefined;
|
||||
switchTheme();
|
||||
}
|
||||
|
||||
// Avoid to be stuck in an endless loop if there is an issue in the stylesheet loading
|
||||
counter++;
|
||||
if (counter === 10) {
|
||||
clearInterval(intervalId);
|
||||
reject();
|
||||
}
|
||||
}, 200);
|
||||
|
||||
styleSheet.onload = () => {
|
||||
clearInterval(intervalId);
|
||||
switchTheme();
|
||||
};
|
||||
|
||||
styleSheet.onerror = (e) => {
|
||||
clearInterval(intervalId);
|
||||
reject(e);
|
||||
};
|
||||
}
|
||||
|
||||
if (cssLoaded) {
|
||||
styleElements[stylesheetName].onload = undefined;
|
||||
switchTheme();
|
||||
}
|
||||
});
|
||||
waitForStyleSheetLoading();
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
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, { useCallback, useEffect } from 'react';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { _t } from '../languageHandler';
|
||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import { ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import ToastStore from "../stores/ToastStore";
|
||||
import AccessibleTooltipButton from "../components/views/elements/AccessibleTooltipButton";
|
||||
import {
|
||||
LiveContentSummary,
|
||||
LiveContentSummaryWithCall,
|
||||
LiveContentType,
|
||||
} from "../components/views/rooms/LiveContentSummary";
|
||||
import { useCall, useJoinCallButtonDisabledTooltip } from "../hooks/useCall";
|
||||
import { useRoomState } from "../hooks/useRoomState";
|
||||
import { ButtonEvent } from "../components/views/elements/AccessibleButton";
|
||||
import { useDispatcher } from "../hooks/useDispatcher";
|
||||
import { ActionPayload } from "../dispatcher/payloads";
|
||||
import { Call } from "../models/Call";
|
||||
|
||||
export const getIncomingCallToastKey = (stateKey: string) => `call_${stateKey}`;
|
||||
|
||||
interface JoinCallButtonWithCallProps {
|
||||
onClick: (e: ButtonEvent) => void;
|
||||
call: Call;
|
||||
}
|
||||
|
||||
function JoinCallButtonWithCall({ onClick, call }: JoinCallButtonWithCallProps) {
|
||||
const tooltip = useJoinCallButtonDisabledTooltip(call);
|
||||
|
||||
return <AccessibleTooltipButton
|
||||
className="mx_IncomingCallToast_joinButton"
|
||||
onClick={onClick}
|
||||
disabled={Boolean(tooltip)}
|
||||
tooltip={tooltip}
|
||||
kind="primary"
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleTooltipButton>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
callEvent: MatrixEvent;
|
||||
}
|
||||
|
||||
export function IncomingCallToast({ callEvent }: Props) {
|
||||
const roomId = callEvent.getRoomId()!;
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
const call = useCall(roomId);
|
||||
|
||||
const dismissToast = useCallback((): void => {
|
||||
ToastStore.sharedInstance().dismissToast(getIncomingCallToastKey(callEvent.getStateKey()!));
|
||||
}, [callEvent]);
|
||||
|
||||
const latestEvent = useRoomState(room, useCallback((state) => {
|
||||
return state.getStateEvents(callEvent.getType(), callEvent.getStateKey()!);
|
||||
}, [callEvent]));
|
||||
|
||||
useEffect(() => {
|
||||
if ("m.terminated" in latestEvent.getContent()) {
|
||||
dismissToast();
|
||||
}
|
||||
}, [latestEvent, dismissToast]);
|
||||
|
||||
useDispatcher(defaultDispatcher, useCallback((payload: ActionPayload) => {
|
||||
if (
|
||||
payload.action === Action.ViewRoom
|
||||
&& payload.room_id === roomId
|
||||
&& payload.view_call
|
||||
) {
|
||||
dismissToast();
|
||||
}
|
||||
}, [roomId, dismissToast]));
|
||||
|
||||
const onJoinClick = useCallback((e: ButtonEvent): void => {
|
||||
e.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>({
|
||||
action: Action.ViewRoom,
|
||||
room_id: room.roomId,
|
||||
view_call: true,
|
||||
metricsTrigger: undefined,
|
||||
});
|
||||
dismissToast();
|
||||
}, [room, dismissToast]);
|
||||
|
||||
const onCloseClick = useCallback((e: ButtonEvent): void => {
|
||||
e.stopPropagation();
|
||||
|
||||
dismissToast();
|
||||
}, [dismissToast]);
|
||||
|
||||
return <React.Fragment>
|
||||
<RoomAvatar
|
||||
room={room ?? undefined}
|
||||
height={24}
|
||||
width={24}
|
||||
/>
|
||||
<div className="mx_IncomingCallToast_content">
|
||||
<div className="mx_IncomingCallToast_info">
|
||||
<span className="mx_IncomingCallToast_room">
|
||||
{ room ? room.name : _t("Unknown room") }
|
||||
</span>
|
||||
<div className="mx_IncomingCallToast_message">
|
||||
{ _t("Video call started") }
|
||||
</div>
|
||||
{ call
|
||||
? <LiveContentSummaryWithCall call={call} />
|
||||
: <LiveContentSummary
|
||||
type={LiveContentType.Video}
|
||||
text={_t("Video")}
|
||||
active={false}
|
||||
participantCount={0}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
{ call
|
||||
? <JoinCallButtonWithCall onClick={onJoinClick} call={call} />
|
||||
: <AccessibleTooltipButton
|
||||
className="mx_IncomingCallToast_joinButton"
|
||||
onClick={onJoinClick}
|
||||
kind="primary"
|
||||
>
|
||||
{ _t("Join") }
|
||||
</AccessibleTooltipButton>
|
||||
}
|
||||
</div>
|
||||
<AccessibleTooltipButton
|
||||
className="mx_IncomingCallToast_closeButton"
|
||||
onClick={onCloseClick}
|
||||
title={_t("Close")}
|
||||
/>
|
||||
</React.Fragment>;
|
||||
}
|
|
@ -512,6 +512,7 @@ export default class WidgetUtils {
|
|||
'theme=$theme',
|
||||
'roomName=$roomName',
|
||||
`supportsScreensharing=${PlatformPeg.get().supportsJitsiScreensharing()}`,
|
||||
'language=$org.matrix.msc2873.client_language',
|
||||
];
|
||||
if (opts.auth) {
|
||||
queryStringParts.push(`auth=${opts.auth}`);
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
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 UAParser from 'ua-parser-js';
|
||||
|
||||
export enum DeviceType {
|
||||
Desktop = 'Desktop',
|
||||
Mobile = 'Mobile',
|
||||
Web = 'Web',
|
||||
Unknown = 'Unknown',
|
||||
}
|
||||
export type ExtendedDeviceInformation = {
|
||||
deviceType: DeviceType;
|
||||
// eg Google Pixel 6
|
||||
deviceModel?: string;
|
||||
// eg Android 11
|
||||
deviceOperatingSystem?: string;
|
||||
// eg Firefox 1.1.0
|
||||
client?: string;
|
||||
};
|
||||
|
||||
// Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)
|
||||
const IOS_KEYWORD = "; iOS ";
|
||||
const BROWSER_KEYWORD = "Mozilla/";
|
||||
|
||||
const getDeviceType = (
|
||||
userAgent: string,
|
||||
device: UAParser.IDevice,
|
||||
browser: UAParser.IBrowser,
|
||||
operatingSystem: UAParser.IOS,
|
||||
): DeviceType => {
|
||||
if (browser.name === 'Electron') {
|
||||
return DeviceType.Desktop;
|
||||
}
|
||||
if (!!browser.name) {
|
||||
return DeviceType.Web;
|
||||
}
|
||||
if (
|
||||
device.type === 'mobile' ||
|
||||
operatingSystem.name?.includes('Android') ||
|
||||
userAgent.indexOf(IOS_KEYWORD) > -1
|
||||
) {
|
||||
return DeviceType.Mobile;
|
||||
}
|
||||
return DeviceType.Unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* Some mobile model and OS strings are not recognised
|
||||
* by the UA parsing library
|
||||
* check they exist by hand
|
||||
*/
|
||||
const checkForCustomValues = (userAgent: string): {
|
||||
customDeviceModel?: string;
|
||||
customDeviceOS?: string;
|
||||
} => {
|
||||
if (userAgent.includes(BROWSER_KEYWORD)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mightHaveDevice = userAgent.includes('(');
|
||||
if (!mightHaveDevice) {
|
||||
return {};
|
||||
}
|
||||
const deviceInfoSegments = userAgent.substring(userAgent.indexOf('(') + 1).split('; ');
|
||||
const customDeviceModel = deviceInfoSegments[0] || undefined;
|
||||
const customDeviceOS = deviceInfoSegments[1] || undefined;
|
||||
return { customDeviceModel, customDeviceOS };
|
||||
};
|
||||
|
||||
const concatenateNameAndVersion = (name?: string, version?: string): string | undefined =>
|
||||
name && [name, version].filter(Boolean).join(' ');
|
||||
|
||||
export const parseUserAgent = (userAgent?: string): ExtendedDeviceInformation => {
|
||||
if (!userAgent) {
|
||||
return {
|
||||
deviceType: DeviceType.Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
const parser = new UAParser(userAgent);
|
||||
|
||||
const browser = parser.getBrowser();
|
||||
const device = parser.getDevice();
|
||||
const operatingSystem = parser.getOS();
|
||||
|
||||
const deviceOperatingSystem = concatenateNameAndVersion(operatingSystem.name, operatingSystem.version);
|
||||
const deviceModel = concatenateNameAndVersion(device.vendor, device.model);
|
||||
const client = concatenateNameAndVersion(browser.name, browser.major || browser.version);
|
||||
|
||||
const { customDeviceModel, customDeviceOS } = checkForCustomValues(userAgent);
|
||||
const deviceType = getDeviceType(userAgent, device, browser, operatingSystem);
|
||||
|
||||
return {
|
||||
deviceType,
|
||||
deviceModel: deviceModel || customDeviceModel,
|
||||
deviceOperatingSystem: deviceOperatingSystem || customDeviceOS,
|
||||
client,
|
||||
};
|
||||
};
|
|
@ -14,14 +14,40 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { LOCAL_NOTIFICATION_SETTINGS_PREFIX } from "matrix-js-sdk/src/@types/event";
|
||||
import { LocalNotificationSettings } from "matrix-js-sdk/src/@types/local_notifications";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import SettingsStore from "../settings/SettingsStore";
|
||||
|
||||
export const deviceNotificationSettingsKeys = [
|
||||
"notificationsEnabled",
|
||||
"notificationBodyEnabled",
|
||||
"audioNotificationsEnabled",
|
||||
];
|
||||
|
||||
export function getLocalNotificationAccountDataEventType(deviceId: string): string {
|
||||
return `${LOCAL_NOTIFICATION_SETTINGS_PREFIX.name}.${deviceId}`;
|
||||
}
|
||||
|
||||
export async function createLocalNotificationSettingsIfNeeded(cli: MatrixClient): Promise<void> {
|
||||
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId);
|
||||
const event = cli.getAccountData(eventType);
|
||||
// New sessions will create an account data event to signify they support
|
||||
// remote toggling of push notifications on this device. Default `is_silenced=true`
|
||||
// For backwards compat purposes, older sessions will need to check settings value
|
||||
// to determine what the state of `is_silenced`
|
||||
if (!event) {
|
||||
// If any of the above is true, we fall in the "backwards compat" case,
|
||||
// and `is_silenced` will be set to `false`
|
||||
const isSilenced = !deviceNotificationSettingsKeys.some(key => SettingsStore.getValue(key));
|
||||
|
||||
await cli.setAccountData(eventType, {
|
||||
is_silenced: isSilenced,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function localNotificationsAreSilenced(cli: MatrixClient): boolean {
|
||||
const eventType = getLocalNotificationAccountDataEventType(cli.deviceId);
|
||||
const event = cli.getAccountData(eventType);
|
||||
|
|
|
@ -43,17 +43,11 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
return `${this.elementUrl}/#/user/${userId}`;
|
||||
}
|
||||
|
||||
forGroup(groupId: string): string {
|
||||
return `${this.elementUrl}/#/group/${groupId}`;
|
||||
}
|
||||
|
||||
forEntity(entityId: string): string {
|
||||
if (entityId[0] === '!' || entityId[0] === '#') {
|
||||
return this.forRoom(entityId);
|
||||
} else if (entityId[0] === '@') {
|
||||
return this.forUser(entityId);
|
||||
} else if (entityId[0] === '+') {
|
||||
return this.forGroup(entityId);
|
||||
} else throw new Error("Unrecognized entity");
|
||||
}
|
||||
|
||||
|
@ -107,8 +101,6 @@ export default class ElementPermalinkConstructor extends PermalinkConstructor {
|
|||
const eventId = parts.length > 2 ? parts.slice(2).join('/') : "";
|
||||
const via = query.split(/&?via=/).filter(p => !!p);
|
||||
return PermalinkParts.forEvent(entity, eventId, via);
|
||||
} else if (entityType === 'group') {
|
||||
return PermalinkParts.forGroup(entity);
|
||||
} else {
|
||||
throw new Error("Unknown entity type in permalink");
|
||||
}
|
||||
|
|
|
@ -51,10 +51,6 @@ export default class MatrixSchemePermalinkConstructor extends PermalinkConstruct
|
|||
return `matrix:${this.encodeEntity(userId)}`;
|
||||
}
|
||||
|
||||
forGroup(groupId: string): string {
|
||||
throw new Error("Deliberately not implemented");
|
||||
}
|
||||
|
||||
forEntity(entityId: string): string {
|
||||
return `matrix:${this.encodeEntity(entityId)}`;
|
||||
}
|
||||
|
|
|
@ -39,10 +39,6 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor {
|
|||
return `${baseUrl}/#/${userId}`;
|
||||
}
|
||||
|
||||
forGroup(groupId: string): string {
|
||||
return `${baseUrl}/#/${groupId}`;
|
||||
}
|
||||
|
||||
forEntity(entityId: string): string {
|
||||
return `${baseUrl}/#/${entityId}`;
|
||||
}
|
||||
|
@ -82,8 +78,6 @@ export default class MatrixToPermalinkConstructor extends PermalinkConstructor {
|
|||
const via = query.split(/&?via=/g).filter(p => !!p);
|
||||
|
||||
return PermalinkParts.forEvent(entity, eventId, via);
|
||||
} else if (entity[0] === '+') {
|
||||
return PermalinkParts.forGroup(entity);
|
||||
} else {
|
||||
throw new Error("Unknown entity type in permalink");
|
||||
}
|
||||
|
|