Merge remote-tracking branch 'origin/develop' into feat/add-message-edition-wysiwyg-composer

This commit is contained in:
Florian Duros 2022-10-21 10:15:46 +02:00
commit 4d089dcc05
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
109 changed files with 2374 additions and 1011 deletions

View file

@ -155,17 +155,6 @@ jobs:
cypress/videos
cypress/synapselogs
- run: mv cypress/performance/*.json cypress/performance/measurements-${{ strategy.job-index }}.json
continue-on-error: true
- name: Upload Benchmark
uses: actions/upload-artifact@v2
with:
name: cypress-benchmark
path: cypress/performance/*
if-no-files-found: ignore
retention-days: 1
report:
name: Report results
needs: tests
@ -181,36 +170,3 @@ jobs:
context: ${{ github.workflow }} / cypress (${{ github.event.workflow_run.event }} => ${{ github.event_name }})
sha: ${{ github.event.workflow_run.head_sha }}
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
store-benchmark:
needs: tests
runs-on: ubuntu-latest
if: |
github.event.workflow_run.event != 'pull_request' &&
github.event.workflow_run.head_branch == 'develop' &&
github.event.workflow_run.head_repository.full_name == github.repository
permissions:
contents: write
steps:
- uses: actions/checkout@v2
- name: Download benchmark result
uses: actions/download-artifact@v3
with:
name: cypress-benchmark
- name: Merge measurements
run: jq -s add measurements-*.json > measurements.json
- name: Store benchmark result
uses: matrix-org/github-action-benchmark@jsperfentry-6
with:
name: Cypress measurements
tool: 'jsperformanceentry'
output-file-path: measurements.json
# The dashboard is available at https://matrix-org.github.io/matrix-react-sdk/cypress/bench/
benchmark-data-dir-path: cypress/bench
fail-on-alert: false
comment-on-alert: false
github-token: ${{ secrets.GITHUB_TOKEN }}
auto-push: ${{ github.event.workflow_run.event != 'pull_request' }}

View file

@ -54,12 +54,10 @@ describe("Create Room", () => {
// Fill room address
cy.get('[label="Room address"]').type("test-room-1");
// Submit
cy.startMeasuring("from-submit-to-room");
cy.get(".mx_Dialog_primary").click();
});
cy.url().should("contain", "/#/room/#test-room-1:localhost");
cy.stopMeasuring("from-submit-to-room");
cy.contains(".mx_RoomHeader_nametext", name);
cy.contains(".mx_RoomHeader_topic", topic);
});

View file

@ -52,11 +52,9 @@ describe("Login", () => {
cy.get("#mx_LoginForm_username").type(username);
cy.get("#mx_LoginForm_password").type(password);
cy.startMeasuring("from-submit-to-home");
cy.get(".mx_Login_submit").click();
cy.url().should('contain', '/#/home', { timeout: 30000 });
cy.stopMeasuring("from-submit-to-home");
});
});

View file

@ -231,6 +231,9 @@ describe("Polls", () => {
// Bot votes 'Maybe' in the poll
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
// wait for bot's vote to arrive
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
// Open context menu
getPollTile(pollId).rightclick();

View file

@ -55,7 +55,6 @@ describe("Registration", () => {
cy.get("#mx_RegistrationForm_username").type("alice");
cy.get("#mx_RegistrationForm_password").type("totally a great password");
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
cy.startMeasuring("create-account");
cy.get(".mx_Login_submit").click();
cy.get(".mx_RegistrationEmailPromptDialog").should("be.visible");
@ -63,13 +62,11 @@ describe("Registration", () => {
cy.checkA11y();
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
cy.stopMeasuring("create-account");
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy").should("be.visible");
cy.percySnapshot("Registration terms prompt", { percyCSS });
cy.checkA11y();
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
cy.startMeasuring("from-submit-to-home");
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
cy.get(".mx_UseCaseSelection_skip", { timeout: 30000 }).should("exist");
@ -78,7 +75,6 @@ describe("Registration", () => {
cy.get(".mx_UseCaseSelection_skip .mx_AccessibleButton").click();
cy.url().should('contain', '/#/home');
cy.stopMeasuring("from-submit-to-home");
cy.get('[aria-label="User menu"]').click();
cy.get('[aria-label="Security & Privacy"]').click();

View file

@ -18,7 +18,6 @@ limitations under the License.
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
import { performance } from "./performance";
import { synapseDocker } from "./synapsedocker";
import { slidingSyncProxyDocker } from "./sliding-sync";
import { webserver } from "./webserver";
@ -30,7 +29,6 @@ import { log } from "./log";
*/
export default function(on: PluginEvents, config: PluginConfigOptions) {
docker(on, config);
performance(on, config);
synapseDocker(on, config);
slidingSyncProxyDocker(on, config);
webserver(on, config);

View file

@ -1,47 +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.
*/
/// <reference types="cypress" />
import * as path from "path";
import * as fse from "fs-extra";
import PluginEvents = Cypress.PluginEvents;
import PluginConfigOptions = Cypress.PluginConfigOptions;
// This holds all the performance measurements throughout the run
let bufferedMeasurements: PerformanceEntry[] = [];
function addMeasurements(measurements: PerformanceEntry[]): void {
bufferedMeasurements = bufferedMeasurements.concat(measurements);
return null;
}
async function writeMeasurementsFile() {
try {
const measurementsPath = path.join("cypress", "performance", "measurements.json");
await fse.outputJSON(measurementsPath, bufferedMeasurements, {
spaces: 4,
});
} finally {
bufferedMeasurements = [];
}
}
export function performance(on: PluginEvents, config: PluginConfigOptions) {
on("task", { addMeasurements });
on("after:run", writeMeasurementsFile);
}

View file

@ -19,7 +19,6 @@ limitations under the License.
import "@percy/cypress";
import "cypress-real-events";
import "./performance";
import "./synapse";
import "./login";
import "./labs";

View file

@ -1,74 +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.
*/
/// <reference types="cypress" />
import Chainable = Cypress.Chainable;
import AUTWindow = Cypress.AUTWindow;
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace Cypress {
interface Chainable {
/**
* Start measuring the duration of some task.
* @param task The task name.
*/
startMeasuring(task: string): Chainable<AUTWindow>;
/**
* Stop measuring the duration of some task.
* The duration is reported in the Cypress log.
* @param task The task name.
*/
stopMeasuring(task: string): Chainable<AUTWindow>;
}
}
}
function getPrefix(task: string): string {
return `cy:${Cypress.spec.name.split(".")[0]}:${task}`;
}
function startMeasuring(task: string): Chainable<AUTWindow> {
return cy.window({ log: false }).then((win) => {
win.mxPerformanceMonitor.start(getPrefix(task));
});
}
function stopMeasuring(task: string): Chainable<AUTWindow> {
return cy.window({ log: false }).then((win) => {
const measure = win.mxPerformanceMonitor.stop(getPrefix(task));
cy.log(`**${task}** ${measure.duration} ms`);
});
}
Cypress.Commands.add("startMeasuring", startMeasuring);
Cypress.Commands.add("stopMeasuring", stopMeasuring);
Cypress.on("window:before:unload", (event: BeforeUnloadEvent) => {
const doc = event.target as Document;
if (doc.location.href === "about:blank") return;
const win = doc.defaultView as AUTWindow;
if (!win.mxPerformanceMonitor) return;
const entries = win.mxPerformanceMonitor.getEntries().filter(entry => {
return entry.name.startsWith("cy:");
});
if (!entries || entries.length === 0) return;
cy.task("addMeasurements", entries);
});
// Needed to make this file a module
export { };

View file

@ -4,7 +4,7 @@
@import "./_font-sizes.pcss";
@import "./_font-weights.pcss";
@import "./_spacing.pcss";
@import "./components/atoms/_Icon.pcss";
@import "./compound/_Icon.pcss";
@import "./components/views/beacon/_BeaconListItem.pcss";
@import "./components/views/beacon/_BeaconStatus.pcss";
@import "./components/views/beacon/_BeaconStatusTooltip.pcss";
@ -371,6 +371,7 @@
@import "./views/voip/_VideoFeed.pcss";
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastPlaybackBody.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastRecordingBody.pcss";

View file

@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/*
* Compound icon
* {@link https://www.figma.com/file/X4XTH9iS2KGJ2wFKDqkyed}
*/
.mx_Icon {
box-sizing: border-box;
display: inline-block;
mask-origin: content-box;
mask-position: center;
mask-repeat: no-repeat;
mask-size: contain;
padding: 1px;
}
@ -28,15 +29,3 @@ limitations under the License.
height: 16px;
width: 16px;
}
.mx_Icon_accent {
background-color: $accent;
}
.mx_Icon_live-badge {
background-color: #fff;
}
.mx_Icon_compound-secondary-content {
background-color: $secondary-content;
}

View file

@ -41,14 +41,14 @@ limitations under the License.
&::before {
display: inline-block;
vertical-align: text-top;
vertical-align: middle;
content: "";
background-color: $secondary-content;
mask-size: 16px;
mask-position-y: center;
width: 16px;
height: 16px;
margin-right: 4px;
bottom: 2px;
height: 1.2em; /* to match line height */
margin-right: 8px;
mask-image: url("$(res)/img/element-icons/call/video-call.svg");
}
}

View file

@ -0,0 +1,31 @@
/*
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_VoiceBroadcastControl {
align-items: center;
background-color: $background;
border-radius: 50%;
color: $secondary-content;
display: flex;
height: 32px;
justify-content: center;
margin-bottom: $spacing-8;
width: 32px;
}
.mx_VoiceBroadcastControl-recording {
color: $alert;
}

View file

@ -31,5 +31,5 @@ limitations under the License.
.mx_VoiceBroadcastRecordingPip_controls {
display: flex;
justify-content: center;
justify-content: space-around;
}

View file

@ -0,0 +1,3 @@
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="5" cy="5" r="5" fill="#FF5B55"/>
</svg>

After

Width:  |  Height:  |  Size: 148 B

View file

@ -1,3 +1,3 @@
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.973633" y="2" width="12" height="12" rx="1" fill="#737D8C"/>
<rect x="0.973633" y="2" width="12" height="12" rx="1" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 180 B

View file

@ -5,54 +5,23 @@
viewBox="0 0 21.799 21.799"
fill="none"
version="1.1"
id="svg12"
sodipodi:docname="live.svg"
inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs16" />
<sodipodi:namedview
id="namedview14"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="35.416667"
inkscape:cx="8.7670588"
inkscape:cy="8.1882353"
inkscape:window-width="1920"
inkscape:window-height="1053"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="svg12" />
<path
d="m 19.188575,3.0855891 c -0.3391,-0.43594 -0.9674,-0.51448 -1.4033,-0.17541 -0.4355,0.3387 -0.5143,0.96596 -0.1766,1.40185 l 8e-4,9.8e-4 0.001,0.00129 0.0136,0.01816 c 0.0133,0.0178 0.0346,0.04686 0.0629,0.0867 0.0566,0.07972 0.1408,0.20227 0.2435,0.36368 0.2058,0.32333 0.4838,0.79947 0.7625,1.39672 0.5603,1.20054 1.1062,2.85339 1.1062,4.7199899 0,1.8666 -0.5459,3.5194 -1.1062,4.72 -0.2787,0.5972 -0.5567,1.0733 -0.7625,1.3967 -0.1027,0.1614 -0.1869,0.2839 -0.2435,0.3637 -0.0283,0.0398 -0.0496,0.0689 -0.0629,0.0867 l -0.0136,0.0181 -0.001,0.0013 -9e-4,0.0012 c -0.3376,0.4358 -0.2587,1.063 0.1767,1.4016 0.4359,0.3391 1.0642,0.2606 1.4033,-0.1754 l -0.7453,-0.5796 c 0.7453,0.5796 0.7453,0.5796 0.7453,0.5796 l 0.002,-0.0025 0.0028,-0.0038 0.0083,-0.0108 0.0265,-0.0352 c 0.0219,-0.0294 0.0521,-0.0707 0.0895,-0.1232 0.0746,-0.1051 0.1779,-0.2558 0.3002,-0.448 0.2442,-0.3838 0.5662,-0.9362 0.8875,-1.6247 0.6397,-1.3709 1.2938,-3.318 1.2938,-5.5657 0,-2.2477199 -0.6541,-4.1948699 -1.2938,-5.5657599 -0.3213,-0.68846 -0.6433,-1.2409 -0.8875,-1.6247 -0.1223,-0.19217 -0.2256,-0.34283 -0.3002,-0.44793 -0.0374,-0.05258 -0.0676,-0.09382 -0.0895,-0.12323 l -0.0265,-0.03521 -0.0083,-0.01085 -0.0028,-0.00371 -0.0012,-0.00143 c 0,0 -8e-4,-0.00114 -0.7902,0.6128 z"
fill="#c1c6cd"
id="path2" />
fill="currentColor" />
<path
d="m 16.589375,6.6858091 c -0.339,-0.43595 -0.9673,-0.51448 -1.4033,-0.17541 -0.4348,0.33819 -0.514,0.96407 -0.178,1.39987 l 0.0034,0.00458 c 0.0045,0.00599 0.0129,0.01748 0.0248,0.03422 0.0238,0.03351 0.0612,0.08776 0.1076,0.16077 0.0933,0.14655 0.2213,0.36554 0.35,0.64137 0.2603,0.55764 0.5062,1.3105399 0.5062,2.1485399 0,0.838 -0.2459,1.5909 -0.5062,2.1485 -0.1287,0.2759 -0.2567,0.4949 -0.35,0.6414 -0.0464,0.073 -0.0838,0.1273 -0.1076,0.1608 -0.0119,0.0167 -0.0203,0.0282 -0.0248,0.0342 l -0.0034,0.0046 c -0.336,0.4358 -0.2568,1.0617 0.178,1.3999 0.436,0.339 1.0643,0.2605 1.4033,-0.1755 l -0.7893,-0.6139 c 0.7893,0.6139 0.7893,0.6139 0.7893,0.6139 l 0.0018,-0.0022 0.0022,-0.0029 0.0058,-0.0075 0.0164,-0.0219 c 0.0131,-0.0176 0.0305,-0.0412 0.0514,-0.0707 0.0418,-0.0589 0.0982,-0.1413 0.1643,-0.245 0.1317,-0.2071 0.3037,-0.5024 0.475,-0.8694 0.3397,-0.728 0.6938,-1.7752 0.6938,-2.9943 0,-1.2190999 -0.3541,-2.2662799 -0.6938,-2.9943099 -0.1713,-0.36703 -0.3433,-0.66233 -0.475,-0.86935 -0.0661,-0.10377 -0.1225,-0.18613 -0.1643,-0.24503 -0.0209,-0.02947 -0.0383,-0.05314 -0.0514,-0.07074 l -0.0164,-0.02187 -0.0058,-0.00748 -0.0022,-0.00287 -9e-4,-0.00122 c 0,0 -9e-4,-0.00107 -0.7902,0.61287 z"
fill="#c1c6cd"
id="path4" />
fill="currentColor" />
<path
d="m 2.6104749,18.713449 c 0.33907,0.4359 0.96734,0.5144 1.40329,0.1754 0.43547,-0.3387 0.5143,-0.966 0.17653,-1.4019 l -7.6e-4,-10e-4 -9.7e-4,-0.0013 -0.01365,-0.0181 c -0.01325,-0.0178 -0.0346,-0.0469 -0.06289,-0.0867 -0.05661,-0.0797 -0.14082,-0.2023 -0.24354,-0.3637 -0.20576,-0.3233 -0.48376,-0.7995 -0.76248,-1.3967 -0.56025,-1.2006 -1.10618,-2.8534 -1.10618,-4.72 0,-1.8665999 0.54593,-3.5194099 1.10618,-4.7199499 0.27872,-0.59726 0.55672,-1.07339 0.76248,-1.39673 0.10272,-0.1614 0.18693,-0.28396 0.24354,-0.36368 0.02829,-0.03983 0.04964,-0.0689 0.06289,-0.0867 l 0.01365,-0.01815 9.7e-4,-0.00129 9.1e-4,-0.00117 c 0.33761,-0.43588 0.25873,-1.06302 -0.17668,-1.40166 -0.43595,-0.33907 -1.06422,-0.26054 -1.40329,0.17541 l 0.74526,0.57964 c -0.74527,-0.57963 -0.74526,-0.57964 -0.74526,-0.57964 l -0.00199,0.00256 -0.00287,0.00372 -0.0083,0.01085 -0.02649,0.0352 c -0.0219,0.02941 -0.05212,0.07066 -0.08945,0.12323 -0.07464,0.10511 -0.17792,0.25577 -0.30021,0.44793 -0.24424,0.38381 -0.56624,0.93625 -0.88752,1.62471 -0.63975001,1.37088 -1.29382000681,3.31804 -1.29382000681,5.5657199 0,2.2477 0.65406999681,4.1949 1.29382000681,5.5658 0.32128,0.6884 0.64328,1.2409 0.88752,1.6247 0.12229,0.1921 0.22557,0.3428 0.30021,0.4479 0.03733,0.0526 0.06755,0.0938 0.08945,0.1232 l 0.02649,0.0352 0.0083,0.0109 0.00287,0.0037 0.0011,0.0014 c 0,0 8.9e-4,0.0012 0.79024,-0.6128 z"
fill="#c1c6cd"
id="path6" />
fill="currentColor" />
<path
d="m 5.2095949,15.113149 c 0.33907,0.436 0.96735,0.5145 1.40329,0.1754 0.43481,-0.3381 0.51407,-0.964 0.17806,-1.3998 l -0.00343,-0.0046 c -0.00447,-0.006 -0.01292,-0.0175 -0.0248,-0.0342 -0.0238,-0.0335 -0.06114,-0.0878 -0.10761,-0.1608 -0.09326,-0.1465 -0.22126,-0.3655 -0.34997,-0.6414 -0.26026,-0.5576 -0.50619,-1.3105 -0.50619,-2.1485 0,-0.838 0.24593,-1.5908999 0.50619,-2.1485499 0.12871,-0.27582 0.25671,-0.49481 0.34997,-0.64136 0.04647,-0.07302 0.08381,-0.12727 0.10761,-0.16078 0.01188,-0.01673 0.02033,-0.02822 0.0248,-0.03422 l 0.00344,-0.00458 c 0.336,-0.43581 0.25674,-1.06168 -0.17807,-1.39986 -0.43594,-0.33907 -1.06422,-0.26054 -1.40329,0.17541 l 0.78935,0.61394 c -0.78935,-0.61394 -0.78935,-0.61394 -0.78935,-0.61394 l -0.00177,0.00228 -0.00222,0.00287 -0.00572,0.00749 -0.01646,0.02186 c -0.01311,0.01761 -0.03044,0.04128 -0.05137,0.07075 -0.04182,0.0589 -0.09823,0.14125 -0.16427,0.24503 -0.13174,0.20702 -0.30374,0.50231 -0.47502,0.86934 -0.33975,0.72803 -0.69382,1.77522 -0.69382,2.9943199 0,1.2191 0.35407,2.2663 0.69382,2.9943 0.17128,0.367 0.34328,0.6623 0.47502,0.8694 0.06604,0.1037 0.12245,0.1861 0.16427,0.245 0.02093,0.0295 0.03826,0.0531 0.05137,0.0707 l 0.01646,0.0219 0.00572,0.0075 0.00222,0.0029 9.4e-4,0.0012 c 0,0 8.3e-4,10e-4 0.79018,-0.6129 z"
fill="#c1c6cd"
id="path8" />
fill="currentColor" />
<circle
cx="10.999774"
cy="10.898949"
r="2"
fill="#c1c6cd"
id="circle10" />
fill="currentColor" />
</svg>

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View file

@ -1,4 +1,4 @@
<svg width="10" height="12" viewBox="0 0 10 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 1C0 0.447715 0.447715 0 1 0H2C2.55228 0 3 0.447715 3 1V11C3 11.5523 2.55228 12 2 12H1C0.447715 12 0 11.5523 0 11V1Z" fill="#737D8C"/>
<path d="M7 1C7 0.447715 7.44772 0 8 0H9C9.55228 0 10 0.447715 10 1V11C10 11.5523 9.55228 12 9 12H8C7.44772 12 7 11.5523 7 11V1Z" fill="#737D8C"/>
<path d="M0 1C0 0.447715 0.447715 0 1 0H2C2.55228 0 3 0.447715 3 1V11C3 11.5523 2.55228 12 2 12H1C0.447715 12 0 11.5523 0 11V1Z" fill="currentColor"/>
<path d="M7 1C7 0.447715 7.44772 0 8 0H9C9.55228 0 10 0.447715 10 1V11C10 11.5523 9.55228 12 9 12H8C7.44772 12 7 11.5523 7 11V1Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 396 B

After

Width:  |  Height:  |  Size: 406 B

View file

@ -1,3 +1,3 @@
<svg width="13" height="16" viewBox="0 0 13 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 14.2104V1.78956C0 1.00724 0.857827 0.527894 1.5241 0.937906L11.6161 7.14834C12.2506 7.53883 12.2506 8.46117 11.6161 8.85166L1.5241 15.0621C0.857828 15.4721 0 14.9928 0 14.2104Z" fill="#737D8C"/>
<path d="M0 14.2104V1.78956C0 1.00724 0.857827 0.527894 1.5241 0.937906L11.6161 7.14834C12.2506 7.53883 12.2506 8.46117 11.6161 8.85166L1.5241 15.0621C0.857828 15.4721 0 14.9928 0 14.2104Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 310 B

After

Width:  |  Height:  |  Size: 315 B

View file

@ -1,3 +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="M8.09999 6.15C8.09999 3.99609 9.84608 2.25 12 2.25C14.1539 2.25 15.9 3.99609 15.9 6.15V11.9825C15.9 14.1365 14.1539 15.8825 12 15.8825C9.84608 15.8825 8.09999 14.1365 8.09999 11.9825V6.15ZM5.1748 10.7375C5.86516 10.7375 6.4248 11.2972 6.4248 11.9875C6.4248 15.0574 8.91483 17.5493 11.9915 17.5538C11.9943 17.5538 11.9972 17.5538 12 17.5538C12.0028 17.5538 12.0056 17.5538 12.0084 17.5538C15.085 17.5492 17.5748 15.0573 17.5748 11.9875C17.5748 11.2972 18.1344 10.7375 18.8248 10.7375C19.5152 10.7375 20.0748 11.2972 20.0748 11.9875C20.0748 16.0189 17.115 19.3576 13.25 19.9577V20.7513C13.25 21.4416 12.6904 22.0013 12 22.0013C11.3096 22.0013 10.75 21.4416 10.75 20.7513V19.9578C6.88482 19.3578 3.9248 16.0191 3.9248 11.9875C3.9248 11.2972 4.48445 10.7375 5.1748 10.7375Z" fill="#737D8C"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.09999 6.15C8.09999 3.99609 9.84608 2.25 12 2.25C14.1539 2.25 15.9 3.99609 15.9 6.15V11.9825C15.9 14.1365 14.1539 15.8825 12 15.8825C9.84608 15.8825 8.09999 14.1365 8.09999 11.9825V6.15ZM5.1748 10.7375C5.86516 10.7375 6.4248 11.2972 6.4248 11.9875C6.4248 15.0574 8.91483 17.5493 11.9915 17.5538C11.9943 17.5538 11.9972 17.5538 12 17.5538C12.0028 17.5538 12.0056 17.5538 12.0084 17.5538C15.085 17.5492 17.5748 15.0573 17.5748 11.9875C17.5748 11.2972 18.1344 10.7375 18.8248 10.7375C19.5152 10.7375 20.0748 11.2972 20.0748 11.9875C20.0748 16.0189 17.115 19.3576 13.25 19.9577V20.7513C13.25 21.4416 12.6904 22.0013 12 22.0013C11.3096 22.0013 10.75 21.4416 10.75 20.7513V19.9578C6.88482 19.3578 3.9248 16.0191 3.9248 11.9875C3.9248 11.2972 4.48445 10.7375 5.1748 10.7375Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 940 B

After

Width:  |  Height:  |  Size: 945 B

View file

@ -39,7 +39,6 @@ import PlatformPeg from "./PlatformPeg";
import { sendLoginRequest } from "./Login";
import * as StorageManager from './utils/StorageManager';
import SettingsStore from "./settings/SettingsStore";
import TypingStore from "./stores/TypingStore";
import ToastStore from "./stores/ToastStore";
import { IntegrationManagers } from "./integrations/IntegrationManagers";
import { Mjolnir } from "./mjolnir/Mjolnir";
@ -62,6 +61,7 @@ import { DialogOpener } from "./utils/DialogOpener";
import { Action } from "./dispatcher/actions";
import AbstractLocalStorageSettingsHandler from "./settings/handlers/AbstractLocalStorageSettingsHandler";
import { OverwriteLoginPayload } from "./dispatcher/payloads/OverwriteLoginPayload";
import { SdkContextClass } from './contexts/SDKContext';
const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url";
@ -797,7 +797,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
dis.dispatch({ action: 'will_start_client' }, true);
// reset things first just in case
TypingStore.sharedInstance().reset();
SdkContextClass.instance.typingStore.reset();
ToastStore.sharedInstance().reset();
DialogOpener.instance.prepare();
@ -927,7 +927,7 @@ export function stopMatrixClient(unsetClient = true): void {
Notifier.stop();
LegacyCallHandler.instance.stop();
UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset();
SdkContextClass.instance.typingStore.reset();
Presence.stop();
ActiveWidgetStore.instance.stop();
IntegrationManagers.sharedInstance().stopWatching();

View file

@ -47,7 +47,7 @@ export const DEFAULTS: IConfigOptions = {
url: "https://element.io/get-started",
},
voice_broadcast: {
chunk_length: 60, // one minute
chunk_length: 120, // two minutes
},
};

View file

@ -1,83 +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 liveIcon from "../../../res/img/element-icons/live.svg";
import microphoneIcon from "../../../res/img/voip/call-view/mic-on.svg";
import pauseIcon from "../../../res/img/element-icons/pause.svg";
import playIcon from "../../../res/img/element-icons/play.svg";
import stopIcon from "../../../res/img/element-icons/Stop.svg";
export enum IconType {
Live,
Microphone,
Pause,
Play,
Stop,
}
const iconTypeMap = new Map([
[IconType.Live, liveIcon],
[IconType.Microphone, microphoneIcon],
[IconType.Pause, pauseIcon],
[IconType.Play, playIcon],
[IconType.Stop, stopIcon],
]);
export enum IconColour {
Accent = "accent",
LiveBadge = "live-badge",
CompoundSecondaryContent = "compound-secondary-content",
}
export enum IconSize {
S16 = "16",
}
interface IconProps {
colour?: IconColour;
size?: IconSize;
type: IconType;
}
export const Icon: React.FC<IconProps> = ({
size = IconSize.S16,
colour = IconColour.Accent,
type,
...rest
}) => {
const classes = [
"mx_Icon",
`mx_Icon_${size}`,
`mx_Icon_${colour}`,
];
const styles: React.CSSProperties = {
maskImage: `url("${iconTypeMap.get(type)}")`,
WebkitMaskImage: `url("${iconTypeMap.get(type)}")`,
};
return (
<i
aria-hidden
className={classes.join(" ")}
role="presentation"
style={styles}
{...rest}
/>
);
};

View file

@ -19,7 +19,7 @@ import React, { useEffect, useState } from "react";
import { _t } from "../../languageHandler";
interface IProps {
parent: HTMLElement;
parent: HTMLElement | null;
onFileDrop(dataTransfer: DataTransfer): void;
}
@ -90,20 +90,20 @@ const FileDropTarget: React.FC<IProps> = ({ parent, onFileDrop }) => {
}));
};
parent.addEventListener("drop", onDrop);
parent.addEventListener("dragover", onDragOver);
parent.addEventListener("dragenter", onDragEnter);
parent.addEventListener("dragleave", onDragLeave);
parent?.addEventListener("drop", onDrop);
parent?.addEventListener("dragover", onDragOver);
parent?.addEventListener("dragenter", onDragEnter);
parent?.addEventListener("dragleave", onDragLeave);
return () => {
// disconnect the D&D event listeners from the room view. This
// is really just for hygiene - we're going to be
// deleted anyway, so it doesn't matter if the event listeners
// don't get cleaned up.
parent.removeEventListener("drop", onDrop);
parent.removeEventListener("dragover", onDragOver);
parent.removeEventListener("dragenter", onDragEnter);
parent.removeEventListener("dragleave", onDragLeave);
parent?.removeEventListener("drop", onDrop);
parent?.removeEventListener("dragover", onDragOver);
parent?.removeEventListener("dragenter", onDragEnter);
parent?.removeEventListener("dragleave", onDragLeave);
};
}, [parent, onFileDrop]);

View file

@ -139,6 +139,7 @@ import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
import { SdkContextClass, SDKContext } from '../../contexts/SDKContext';
import { viewUserDeviceSettings } from '../../actions/handlers/viewUserDeviceSettings';
import { VoiceBroadcastResumer } from '../../voice-broadcast';
// legacy export
export { default as Views } from "../../Views";
@ -234,6 +235,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private focusComposer: boolean;
private subTitleStatus: string;
private prevWindowWidth: number;
private voiceBroadcastResumer: VoiceBroadcastResumer;
private readonly loggedInView: React.RefObject<LoggedInViewType>;
private readonly dispatcherRef: string;
@ -433,6 +435,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
window.removeEventListener("resize", this.onWindowResized);
if (this.accountPasswordTimer !== null) clearTimeout(this.accountPasswordTimer);
if (this.voiceBroadcastResumer) this.voiceBroadcastResumer.destroy();
}
private onWindowResized = (): void => {
@ -1618,6 +1621,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
});
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
}
/**

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { createRef, KeyboardEvent } from 'react';
import { Thread, THREAD_RELATION_TYPE, ThreadEvent } from 'matrix-js-sdk/src/models/thread';
import { Room } from 'matrix-js-sdk/src/models/room';
import { Room, RoomEvent } from 'matrix-js-sdk/src/models/room';
import { IEventRelation, MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import { Direction } from 'matrix-js-sdk/src/models/event-timeline';
@ -70,6 +70,7 @@ interface IProps {
interface IState {
thread?: Thread;
lastReply?: MatrixEvent | null;
layout: Layout;
editState?: EditorStateTransfer;
replyToEvent?: MatrixEvent;
@ -88,9 +89,16 @@ export default class ThreadView extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
const thread = this.props.room.getThread(this.props.mxEvent.getId());
this.setupThreadListeners(thread);
this.state = {
layout: SettingsStore.getValue("layout"),
narrow: false,
thread,
lastReply: thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
}),
};
this.layoutWatcherRef = SettingsStore.watchSetting("layout", null, (...[,,, value]) =>
@ -99,6 +107,9 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
if (this.state.thread) {
this.postThreadUpdate(this.state.thread);
}
this.setupThread(this.props.mxEvent);
this.dispatcherRef = dis.register(this.onAction);
@ -189,19 +200,49 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private updateThreadRelation = (): void => {
this.setState({
lastReply: this.threadLastReply,
});
};
private get threadLastReply(): MatrixEvent | undefined {
return this.state.thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
});
}
private updateThread = (thread?: Thread) => {
if (thread && this.state.thread !== thread) {
if (this.state.thread === thread) return;
this.setupThreadListeners(thread, this.state.thread);
if (thread) {
this.setState({
thread,
}, async () => {
thread.emit(ThreadEvent.ViewThread);
await thread.fetchInitialEvents();
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
this.timelinePanel.current?.refreshTimeline();
});
lastReply: this.threadLastReply,
}, async () => this.postThreadUpdate(thread));
}
};
private async postThreadUpdate(thread: Thread): Promise<void> {
thread.emit(ThreadEvent.ViewThread);
await thread.fetchInitialEvents();
this.updateThreadRelation();
this.nextBatch = thread.liveTimeline.getPaginationToken(Direction.Backward);
this.timelinePanel.current?.refreshTimeline();
}
private setupThreadListeners(thread?: Thread | undefined, oldThread?: Thread | undefined): void {
if (oldThread) {
this.state.thread.off(ThreadEvent.NewReply, this.updateThreadRelation);
this.props.room.off(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
}
if (thread) {
thread.on(ThreadEvent.NewReply, this.updateThreadRelation);
this.props.room.on(RoomEvent.LocalEchoUpdated, this.updateThreadRelation);
}
}
private resetJumpToEvent = (event?: string): void => {
if (this.props.initialEvent && this.props.initialEventScrollIntoView &&
event === this.props.initialEvent?.getId()) {
@ -242,14 +283,14 @@ export default class ThreadView extends React.Component<IProps, IState> {
}
};
private nextBatch: string;
private nextBatch: string | undefined | null = null;
private onPaginationRequest = async (
timelineWindow: TimelineWindow | null,
direction = Direction.Backward,
limit = 20,
): Promise<boolean> => {
if (!Thread.hasServerSideSupport) {
if (!Thread.hasServerSideSupport && timelineWindow) {
timelineWindow.extend(direction, limit);
return true;
}
@ -262,40 +303,50 @@ export default class ThreadView extends React.Component<IProps, IState> {
opts.from = this.nextBatch;
}
const { nextBatch } = await this.state.thread.fetchEvents(opts);
this.nextBatch = nextBatch;
let nextBatch: string | null | undefined = null;
if (this.state.thread) {
const response = await this.state.thread.fetchEvents(opts);
nextBatch = response.nextBatch;
this.nextBatch = nextBatch;
}
// Advances the marker on the TimelineWindow to define the correct
// window of events to display on screen
timelineWindow.extend(direction, limit);
timelineWindow?.extend(direction, limit);
return !!nextBatch;
};
private onFileDrop = (dataTransfer: DataTransfer) => {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(dataTransfer.files),
this.props.mxEvent.getRoomId(),
this.threadRelation,
MatrixClientPeg.get(),
TimelineRenderingType.Thread,
);
const roomId = this.props.mxEvent.getRoomId();
if (roomId) {
ContentMessages.sharedInstance().sendContentListToRoom(
Array.from(dataTransfer.files),
roomId,
this.threadRelation,
MatrixClientPeg.get(),
TimelineRenderingType.Thread,
);
} else {
console.warn("Unknwon roomId for event", this.props.mxEvent);
}
};
private get threadRelation(): IEventRelation {
const lastThreadReply = this.state.thread?.lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name) && !ev.status;
});
return {
const relation = {
"rel_type": THREAD_RELATION_TYPE.name,
"event_id": this.state.thread?.id,
"is_falling_back": true,
"m.in_reply_to": {
"event_id": lastThreadReply?.getId() ?? this.state.thread?.id,
},
};
const fallbackEventId = this.state.lastReply?.getId() ?? this.state.thread?.id;
if (fallbackEventId) {
relation["m.in_reply_to"] = {
"event_id": fallbackEventId,
};
}
return relation;
}
private renderThreadViewHeader = (): JSX.Element => {
@ -314,7 +365,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
const threadRelation = this.threadRelation;
let timeline: JSX.Element;
let timeline: JSX.Element | null;
if (this.state.thread) {
if (this.props.initialEvent && this.props.initialEvent.getRoomId() !== this.state.thread.roomId) {
logger.warn("ThreadView attempting to render TimelinePanel with mismatched initialEvent",

View file

@ -370,7 +370,7 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
}
return (
<div className="mx_LoginWithQR">
<div data-testid="login-with-qr" className="mx_LoginWithQR">
<div className={centreTitle ? "mx_LoginWithQR_centreTitle" : ""}>
{ backButton ?
<AccessibleButton

View file

@ -29,9 +29,9 @@ import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
interface IProps {
export interface ThreadListContextMenuProps {
mxEvent: MatrixEvent;
permalinkCreator: RoomPermalinkCreator;
permalinkCreator?: RoomPermalinkCreator;
onMenuToggle?: (open: boolean) => void;
}
@ -43,7 +43,7 @@ const contextMenuBelow = (elementRect: DOMRect) => {
return { left, top, chevronFace };
};
const ThreadListContextMenu: React.FC<IProps> = ({
const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
mxEvent,
permalinkCreator,
onMenuToggle,
@ -64,12 +64,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
closeThreadOptions();
}, [mxEvent, closeThreadOptions]);
const copyLinkToThread = useCallback(async (evt: ButtonEvent) => {
evt.preventDefault();
evt.stopPropagation();
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
await copyPlaintext(matrixToUrl);
closeThreadOptions();
const copyLinkToThread = useCallback(async (evt: ButtonEvent | undefined) => {
if (permalinkCreator) {
evt?.preventDefault();
evt?.stopPropagation();
const matrixToUrl = permalinkCreator.forEvent(mxEvent.getId());
await copyPlaintext(matrixToUrl);
closeThreadOptions();
}
}, [mxEvent, closeThreadOptions, permalinkCreator]);
useEffect(() => {
@ -87,6 +89,7 @@ const ThreadListContextMenu: React.FC<IProps> = ({
title={_t("Thread options")}
isExpanded={menuDisplayed}
inputRef={button}
data-testid="threadlist-dropdown-button"
/>
{ menuDisplayed && (<IconizedContextMenu
onFinished={closeThreadOptions}
@ -102,11 +105,14 @@ const ThreadListContextMenu: React.FC<IProps> = ({
label={_t("View in room")}
iconClassName="mx_ThreadPanel_viewInRoom"
/> }
<IconizedContextMenuOption
onClick={(e) => copyLinkToThread(e)}
label={_t("Copy link to thread")}
iconClassName="mx_ThreadPanel_copyLinkToThread"
/>
{ permalinkCreator &&
<IconizedContextMenuOption
data-testid="copy-thread-link"
onClick={(e) => copyLinkToThread(e)}
label={_t("Copy link to thread")}
iconClassName="mx_ThreadPanel_copyLinkToThread"
/>
}
</IconizedContextMenuOptionList>
</IconizedContextMenu>) }
</React.Fragment>;

View file

@ -21,10 +21,11 @@ import { logger } from "matrix-js-sdk/src/logger";
import { _t } from "../../../languageHandler";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { OIDCState, WidgetPermissionStore } from "../../../stores/widgets/WidgetPermissionStore";
import { OIDCState } from "../../../stores/widgets/WidgetPermissionStore";
import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog";
import DialogButtons from "../elements/DialogButtons";
import { SdkContextClass } from '../../../contexts/SDKContext';
interface IProps extends IDialogProps {
widget: Widget;
@ -57,7 +58,7 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
if (this.state.rememberSelection) {
logger.log(`Remembering ${this.props.widget.id} as allowed=${allowed} for OpenID`);
WidgetPermissionStore.instance.setOIDCState(
SdkContextClass.instance.widgetPermissionStore.setOIDCState(
this.props.widget, this.props.widgetKind, this.props.inRoomId,
allowed ? OIDCState.Allowed : OIDCState.Denied,
);

View file

@ -40,6 +40,7 @@ export default class Spinner extends React.PureComponent<IProps> {
style={{ width: w, height: h }}
aria-label={_t("Loading...")}
role="progressbar"
data-testid="spinner"
/>
</div>
);

View file

@ -57,7 +57,7 @@ type State = Partial<Pick<CSSProperties, "display" | "right" | "top" | "transfor
export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
private static container: HTMLElement;
private parent: Element;
private parent: Element | null = null;
// XXX: This is because some components (Field) are unable to `import` the Tooltip class,
// so we expose the Alignment options off of us statically.
@ -87,7 +87,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
capture: true,
});
this.parent = ReactDOM.findDOMNode(this).parentNode as Element;
this.parent = ReactDOM.findDOMNode(this)?.parentNode as Element ?? null;
this.updatePosition();
}
@ -109,7 +109,7 @@ export default class Tooltip extends React.PureComponent<ITooltipProps, State> {
// positioned, also taking into account any window zoom
private updatePosition = (): void => {
// When the tooltip is hidden, no need to thrash the DOM with `style` attribute updates (performance)
if (!this.props.visible) return;
if (!this.props.visible || !this.parent) return;
const parentBox = this.parent.getBoundingClientRect();
const width = UIStore.instance.windowWidth;

View file

@ -31,7 +31,6 @@ import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
import { getAutoCompleteCreator, Part, Type } from '../../../editor/parts';
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
import { renderModel } from '../../../editor/render';
import TypingStore from "../../../stores/TypingStore";
import SettingsStore from "../../../settings/SettingsStore";
import { IS_MAC, Key } from "../../../Keyboard";
import { EMOTICON_TO_EMOJI } from "../../../emoji";
@ -47,6 +46,7 @@ import { getKeyBindingsManager } from '../../../KeyBindingsManager';
import { ALTERNATE_KEY_NAME, KeyBindingAction } from '../../../accessibility/KeyboardShortcuts';
import { _t } from "../../../languageHandler";
import { linkify } from '../../../linkify-matrix';
import { SdkContextClass } from '../../../contexts/SDKContext';
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s|:^$');
@ -246,7 +246,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
isTyping = false;
}
}
TypingStore.sharedInstance().setSelfTyping(
SdkContextClass.instance.typingStore.setSelfTyping(
this.props.room.roomId,
this.props.threadId,
isTyping,
@ -789,6 +789,7 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
aria-activedescendant={activeDescendant}
dir="auto"
aria-disabled={this.props.disabled}
data-testid="basicmessagecomposer"
/>
</div>);
}

View file

@ -74,6 +74,7 @@ function SendButton(props: ISendButtonProps) {
className="mx_MessageComposer_sendMessage"
onClick={props.onClick}
title={props.title ?? _t('Send message')}
data-testid="sendmessagebtn"
/>
);
}

View file

@ -379,7 +379,7 @@ const RoomListHeader = ({ onVisibilityChange }: IProps) => {
isExpanded={mainMenuDisplayed}
className="mx_RoomListHeader_contextMenuButton"
title={activeSpace
? _t("%(spaceName)s menu", { spaceName })
? _t("%(spaceName)s menu", { spaceName: spaceName ?? activeSpace.name })
: _t("Home options")}
>
{ title }

View file

@ -25,7 +25,9 @@ import { RoomNotificationStateStore } from "../stores/notifications/RoomNotifica
import RightPanelStore from "../stores/right-panel/RightPanelStore";
import { RoomViewStore } from "../stores/RoomViewStore";
import SpaceStore, { SpaceStoreClass } from "../stores/spaces/SpaceStore";
import TypingStore from "../stores/TypingStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../stores/widgets/WidgetPermissionStore";
import WidgetStore from "../stores/WidgetStore";
export const SDKContext = createContext<SdkContextClass>(undefined);
@ -50,6 +52,7 @@ export class SdkContextClass {
public client?: MatrixClient;
// All protected fields to make it easier to derive test stores
protected _WidgetPermissionStore?: WidgetPermissionStore;
protected _RightPanelStore?: RightPanelStore;
protected _RoomNotificationStateStore?: RoomNotificationStateStore;
protected _RoomViewStore?: RoomViewStore;
@ -59,6 +62,7 @@ export class SdkContextClass {
protected _SlidingSyncManager?: SlidingSyncManager;
protected _SpaceStore?: SpaceStoreClass;
protected _LegacyCallHandler?: LegacyCallHandler;
protected _TypingStore?: TypingStore;
/**
* Automatically construct stores which need to be created eagerly so they can register with
@ -100,6 +104,12 @@ export class SdkContextClass {
}
return this._WidgetLayoutStore;
}
public get widgetPermissionStore(): WidgetPermissionStore {
if (!this._WidgetPermissionStore) {
this._WidgetPermissionStore = new WidgetPermissionStore(this);
}
return this._WidgetPermissionStore;
}
public get widgetStore(): WidgetStore {
if (!this._WidgetStore) {
this._WidgetStore = WidgetStore.instance;
@ -124,4 +134,11 @@ export class SdkContextClass {
}
return this._SpaceStore;
}
public get typingStore(): TypingStore {
if (!this._TypingStore) {
this._TypingStore = new TypingStore(this);
window.mxTypingStore = this._TypingStore;
}
return this._TypingStore;
}
}

View file

@ -941,7 +941,7 @@
"You can reset your password, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete si změnit heslo, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.",
"You can log in, but some features will be unavailable until the identity server is back online. If you keep seeing this warning, check your configuration or contact a server admin.": "Můžete se přihlásit, ale některé funkce nebudou dostupné dokud nezačne server identity fungovat. Pokud se vám toto varování zobrazuje i nadále, zkontrolujte svojí konfiguraci nebo kontaktujte správce serveru.",
"Call failed due to misconfigured server": "Volání selhalo, protože je rozbitá konfigurace serveru",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého homeserveru (<code>%(homeserverDomain)s</code>) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.",
"Please ask the administrator of your homeserver (<code>%(homeserverDomain)s</code>) to configure a TURN server in order for calls to work reliably.": "Požádejte správce svého domovského serveru (<code>%(homeserverDomain)s</code>) jestli by nemohl nakonfigurovat TURN server, aby volání fungovala spolehlivě.",
"Alternatively, you can try to use the public server at <code>turn.matrix.org</code>, but this will not be as reliable, and it will share your IP address with that server. You can also manage this in Settings.": "Můžete také zkusit použít veřejný server na adrese <code>turn.matrix.org</code>, ale ten nebude tak spolehlivý a bude sdílet vaši IP adresu s tímto serverem. To můžete spravovat také v Nastavení.",
"Try using turn.matrix.org": "Zkuste použít turn.matrix.org",
"Messages": "Zprávy",
@ -1441,7 +1441,7 @@
"Manually Verify by Text": "Manuální textové ověření",
"Interactively verify by Emoji": "Interaktivní ověření s emotikonami",
"Support adding custom themes": "Umožnit přidání vlastního vzhledu",
"Manually verify all remote sessions": "Manuálně ověřit všechny relace",
"Manually verify all remote sessions": "Ručně ověřit všechny relace",
"cached locally": "uložen lokálně",
"not found locally": "nenalezen lolálně",
"Individually verify each session used by a user to mark it as trusted, not trusting cross-signed devices.": "Individuálně ověřit každou uživatelovu relaci a označit jí za důvěryhodnou, bez důvěry v křížový podpis.",
@ -3635,5 +3635,34 @@
"Notifications silenced": "Oznámení ztlumena",
"Yes, stop broadcast": "Ano, zastavit vysílání",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Opravdu chcete ukončit živé vysílání? Tím se vysílání ukončí a v místnosti bude k dispozici celý záznam.",
"Stop live broadcasting?": "Ukončit živé vysílání?"
"Stop live broadcasting?": "Ukončit živé vysílání?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Hlasové vysílání už nahrává někdo jiný. Počkejte, až jeho hlasové vysílání skončí, a spusťte nové.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte potřebná oprávnění ke spuštění hlasového vysílání v této místnosti. Obraťte se na správce místnosti, aby vám zvýšil oprávnění.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Již nahráváte hlasové vysílání. Ukončete prosím aktuální hlasové vysílání a spusťte nové.",
"Can't start a new voice broadcast": "Nelze spustit nové hlasové vysílání",
"Completing set up of your new device": "Dokončování nastavení nového zařízení",
"Waiting for device to sign in": "Čekání na přihlášení zařízení",
"Connecting...": "Připojování...",
"Review and approve the sign in": "Zkontrolovat a schválit přihlášení",
"Select 'Scan QR code'": "Vyberte \"Naskenovat QR kód\"",
"Start at the sign in screen": "Začněte na přihlašovací obrazovce",
"Scan the QR code below with your device that's signed out.": "Níže uvedený QR kód naskenujte pomocí přihlašovaného zařízení.",
"By approving access for this device, it will have full access to your account.": "Schválením přístupu tohoto zařízení získá zařízení plný přístup k vašemu účtu.",
"Check that the code below matches with your other device:": "Zkontrolujte, zda se níže uvedený kód shoduje s vaším dalším zařízením:",
"Devices connected": "Zařízení byla propojena",
"The homeserver doesn't support signing in another device.": "Domovský server nepodporuje přihlášení pomocí jiného zařízení.",
"An unexpected error occurred.": "Došlo k neočekávané chybě.",
"The request was cancelled.": "Požadavek byl zrušen.",
"The other device isn't signed in.": "Druhé zařízení není přihlášeno.",
"The other device is already signed in.": "Druhé zařízení je již přihlášeno.",
"The request was declined on the other device.": "Požadavek byl na druhém zařízení odmítnut.",
"Linking with this device is not supported.": "Propojení s tímto zařízením není podporováno.",
"The scanned code is invalid.": "Naskenovaný kód je neplatný.",
"The linking wasn't completed in the required time.": "Propojení nebylo dokončeno v požadovaném čase.",
"Sign in new device": "Přihlásit nové zařízení",
"Show QR code": "Zobrazit QR kód",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Toto zařízení můžete použít k přihlášení nového zařízení pomocí QR kódu. QR kód zobrazený na tomto zařízení musíte naskenovat pomocí odhlášeného zařízení.",
"Sign in with QR code": "Přihlásit se pomocí QR kódu",
"Browser": "Prohlížeč",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Povolit zobrazení QR kódu ve správci relací pro přihlášení do jiného zařízení (vyžaduje kompatibilní domovský server)"
}

View file

@ -919,7 +919,7 @@
"Only continue if you trust the owner of the server.": "Fahre nur fort, wenn du den Server-Betreibenden vertraust.",
"Trust": "Vertrauen",
"Custom (%(level)s)": "Benutzerdefiniert (%(level)s)",
"Sends a message as plain text, without interpreting it as markdown": "Verschickt eine Nachricht in Rohtext, ohne sie als Markdown darzustellen",
"Sends a message as plain text, without interpreting it as markdown": "Sendet eine Nachricht als Klartext, ohne sie als Markdown darzustellen",
"Use an identity server to invite by email. Manage in Settings.": "Verwende einen Identitäts-Server, um per E-Mail einladen zu können. Lege einen in den Einstellungen fest.",
"%(name)s (%(userId)s)": "%(name)s (%(userId)s)",
"Try out new ways to ignore people (experimental)": "Verwende neue Möglichkeiten, Menschen zu blockieren",
@ -1166,7 +1166,7 @@
"%(creator)s created and configured the room.": "%(creator)s hat den Raum erstellt und konfiguriert.",
"Keep a copy of it somewhere secure, like a password manager or even a safe.": "Bewahre eine Kopie an einem sicheren Ort, wie einem Passwort-Manager oder in einem Safe auf.",
"Copy": "Kopieren",
"Sends a message as html, without interpreting it as markdown": "Verschickt eine Nachricht im HTML-Format, ohne sie als Markdown zu darzustellen",
"Sends a message as html, without interpreting it as markdown": "Sendet eine Nachricht als HTML, ohne sie als Markdown darzustellen",
"Show rooms with unread notifications first": "Zeige Räume mit ungelesenen Benachrichtigungen zuerst an",
"Show shortcuts to recently viewed rooms above the room list": "Kürzlich besuchte Räume anzeigen",
"Use Single Sign On to continue": "Einmalanmeldung zum Fortfahren nutzen",
@ -3624,11 +3624,40 @@
"resume voice broadcast": "Sprachübertragung fortsetzen",
"Italic": "Kursiv",
"Underline": "Unterstrichen",
"Try out the rich text editor (plain text mode coming soon)": "Probiere den Rich-Text-Editor aus (bald auch mit Plain-Text-Modus)",
"Try out the rich text editor (plain text mode coming soon)": "Probiere den Textverarbeitungs-Editor (bald auch mit Klartext-Modus)",
"You have already joined this call from another device": "Du nimmst an diesem Anruf bereits mit einem anderen Gerät teil",
"stop voice broadcast": "Sprachübertragung beenden",
"Notifications silenced": "Benachrichtigungen stummgeschaltet",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Willst du die Sprachübertragung wirklich beenden? Damit endet auch die Aufnahme.",
"Yes, stop broadcast": "Ja, Sprachübertragung beenden",
"Stop live broadcasting?": "Sprachübertragung beenden?"
"Stop live broadcasting?": "Sprachübertragung beenden?",
"Sign in with QR code": "Mit QR-Code anmelden",
"Browser": "Browser",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Erlaube es andere Geräte mittels QR-Code in der Sitzungsverwaltung anzumelden (kompatibler Heim-Server benötigt)",
"Completing set up of your new device": "Schließe Anmeldung deines neuen Gerätes ab",
"Waiting for device to sign in": "Warte auf Anmeldung des Gerätes",
"Connecting...": "Verbinde …",
"Review and approve the sign in": "Überprüfe und genehmige die Anmeldung",
"Select 'Scan QR code'": "Wähle „QR-Code einlesen“",
"Start at the sign in screen": "Beginne auf dem Anmeldebildschirm",
"Scan the QR code below with your device that's signed out.": "Lese den folgenden QR-Code mit deinem nicht angemeldeten Gerät ein.",
"By approving access for this device, it will have full access to your account.": "Indem du den Zugriff dieses Gerätes bestätigst, erhält es vollen Zugang zu deinem Account.",
"Check that the code below matches with your other device:": "Überprüfe, dass der unten angezeigte Code mit deinem anderen Gerät übereinstimmt:",
"Devices connected": "Geräte verbunden",
"The homeserver doesn't support signing in another device.": "Der Heim-Server unterstützt die Anmeldung eines anderen Gerätes nicht.",
"An unexpected error occurred.": "Ein unerwarteter Fehler ist aufgetreten.",
"The request was cancelled.": "Die Anfrage wurde abgebrochen.",
"The other device isn't signed in.": "Das andere Gerät ist nicht angemeldet.",
"The other device is already signed in.": "Das andere Gerät ist bereits angemeldet.",
"The request was declined on the other device.": "Die Anfrage wurde auf dem anderen Gerät abgelehnt.",
"Linking with this device is not supported.": "Die Verbindung mit diesem Gerät wird nicht unterstützt.",
"The scanned code is invalid.": "Der gescannte Code ist ungültig.",
"The linking wasn't completed in the required time.": "Die Verbindung konnte nicht in der erforderlichen Zeit hergestellt werden.",
"Sign in new device": "Neues Gerät anmelden",
"Show QR code": "QR-Code anzeigen",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Jemand anderes nimmt bereits eine Sprachübertragung auf. Warte auf das Ende der Übertragung, bevor du eine neue startest.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Du hast nicht die nötigen Berechtigungen, um eine Sprachübertragung in diesem Raum zu starten. Kontaktiere einen Raumadministrator, um deine Berechtigungen anzupassen.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Du zeichnest bereits eine Sprachübertragung auf. Bitte beende die laufende Übertragung, um eine neue zu beginnen.",
"Can't start a new voice broadcast": "Sprachübertragung kann nicht gestartet werden",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Du kannst dieses Gerät verwenden, um ein neues Gerät per QR-Code anzumelden. Dazu musst du den auf diesem Gerät angezeigten QR-Code mit deinem nicht angemeldeten Gerät einlesen."
}

View file

@ -644,10 +644,10 @@
"Stop live broadcasting?": "Stop live broadcasting?",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.",
"Yes, stop broadcast": "Yes, stop broadcast",
"Live": "Live",
"pause voice broadcast": "pause voice broadcast",
"play voice broadcast": "play voice broadcast",
"resume voice broadcast": "resume voice broadcast",
"stop voice broadcast": "stop voice broadcast",
"pause voice broadcast": "pause voice broadcast",
"Live": "Live",
"Voice broadcast": "Voice broadcast",
"Cannot reach homeserver": "Cannot reach homeserver",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin",

View file

@ -3631,5 +3631,37 @@
"New session manager": "Uus sessioonihaldur",
"Use new session manager": "Kasuta uut sessioonihaldurit",
"Try out the rich text editor (plain text mode coming soon)": "Proovi vormindatud teksti alusel töötavat tekstitoimetit (varsti lisandub ka vormindamata teksti režiim)",
"Notifications silenced": "Teavitused on summutatud"
"Notifications silenced": "Teavitused on summutatud",
"Completing set up of your new device": "Lõpetame uue seadme seadistamise",
"Waiting for device to sign in": "Ootame, et teine seade logiks võrku",
"Connecting...": "Ühendamisel…",
"Review and approve the sign in": "Vaata üle ja kinnita sisselogimine Matrixi'i võrku",
"Select 'Scan QR code'": "Vali „Loe QR-koodi“",
"Start at the sign in screen": "Alusta sisselogimisvaatest",
"Scan the QR code below with your device that's signed out.": "Loe QR-koodi seadmega, kus sa oled Matrix'i võrgust välja loginud.",
"By approving access for this device, it will have full access to your account.": "Lubades ligipääsu sellele seadmele, annad talle ka täismahulise ligipääsu oma kasutajakontole.",
"Check that the code below matches with your other device:": "Kontrolli, et järgnev kood klapib teises seadmes kuvatava koodiga:",
"Devices connected": "Seadmed on ühendatud",
"The homeserver doesn't support signing in another device.": "Koduserver ei toeta muude seadmete võrku logimise võimalust.",
"An unexpected error occurred.": "Tekkis teadmata viga.",
"The request was cancelled.": "Päring katkestati.",
"The other device isn't signed in.": "Teine seade ei ole võrku loginud.",
"The other device is already signed in.": "Teine seade on juba võrku loginud.",
"The request was declined on the other device.": "Teine seade lükkas päringu tagasi.",
"Linking with this device is not supported.": "Sidumine selle seadmega ei ole toetatud.",
"The scanned code is invalid.": "Skaneeritud QR-kood on vigane.",
"The linking wasn't completed in the required time.": "Sidumine ei lõppenud etteantud aja jooksul.",
"Sign in new device": "Logi sisse uus seade",
"Show QR code": "Näita QR-koodi",
"Sign in with QR code": "Logi sisse QR-koodi abil",
"Browser": "Brauser",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Sa saad kasutada seda seadet mõne muu seadme logimiseks Matrix'i võrku QR-koodi alusel. Selleks skaneeri võrgust väljalogitud seadmega seda QR-koodi.",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Teise seadme sisselogimiseks luba QR-koodi kuvamine sessioonihalduris (eeldab, et koduserver sellist võimalust toetab)",
"Yes, stop broadcast": "Jah, lõpeta",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Kas sa oled kindel, et soovid otseeetri lõpetada? Sellega ringhäälingukõne salvestamine lõppeb ja salvestis on kättesaadav kõigile jututoas.",
"Stop live broadcasting?": "Kas lõpetame otseeetri?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Keegi juba salvestab ringhäälingukõnet. Uue ringhäälingukõne salvestamiseks palun oota, kuni see teine ringhäälingukõne on lõppenud.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Sul pole piisavalt õigusi selles jututoas ringhäälingukõne algatamiseks. Õiguste lisamiseks palun võta ühendust jututoa haldajaga.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Sa juba salvestad ringhäälingukõnet. Uue alustamiseks palun lõpeta eelmine salvestus.",
"Can't start a new voice broadcast": "Uue ringhäälingukõne alustamine pole võimalik"
}

View file

@ -3631,5 +3631,33 @@
"You do not have sufficient permissions to change this.": "Nincs megfelelő jogosultság a megváltoztatáshoz.",
"%(brand)s is end-to-end encrypted, but is currently limited to smaller numbers of users.": "%(brand)s végpontok között titkosított de jelenleg csak kevés számú résztvevővel működik.",
"Enable %(brand)s as an additional calling option in this room": "%(brand)s engedélyezése mint további opció hívásokhoz a szobában",
"Notifications silenced": "Értesítések elnémítva"
"Notifications silenced": "Értesítések elnémítva",
"Stop live broadcasting?": "Megszakítja az élő közvetítést?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Valaki már elindított egy hang közvetítést. Várja meg a közvetítés végét az új indításához.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nincs jogosultsága hang közvetítést indítani ebben a szobában. Vegye fel a kapcsolatot a szoba adminisztrátorával a szükséges jogosultság megszerzéséhez.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Egy hang közvetítés már folyamatban van. Először fejezze be a jelenlegi közvetítést egy új indításához.",
"Can't start a new voice broadcast": "Az új hang közvetítés nem indítható el",
"Completing set up of your new device": "Új eszköz beállításának elvégzése",
"Waiting for device to sign in": "Várakozás a másik eszköz bejelentkezésére",
"Connecting...": "Kapcsolás…",
"Select 'Scan QR code'": "Válassza ezt: „QR kód beolvasása”",
"Start at the sign in screen": "Kezdje a bejelentkező képernyőn",
"Scan the QR code below with your device that's signed out.": "A kijelentkezett eszközzel olvasd be a QR kódot alább.",
"By approving access for this device, it will have full access to your account.": "Ennek az eszköznek a hozzáférés engedélyezése után az eszköznek teljes hozzáférése lesz a fiókjához.",
"Check that the code below matches with your other device:": "Ellenőrizze, hogy az alábbi kód megegyezik a másik eszközödön lévővel:",
"Devices connected": "Összekötött eszközök",
"The homeserver doesn't support signing in another device.": "A matrix szerver nem támogatja más eszköz bejelentkeztetését.",
"An unexpected error occurred.": "Nemvárt hiba történt.",
"The request was cancelled.": "A kérés megszakítva.",
"The other device isn't signed in.": "A másik eszköz még nincs bejelentkezve.",
"The other device is already signed in.": "A másik eszköz már bejelentkezett.",
"The request was declined on the other device.": "A kérést elutasították a másik eszközön.",
"Linking with this device is not supported.": "Összekötés ezzel az eszközzel nem támogatott.",
"The scanned code is invalid.": "A beolvasott kód érvénytelen.",
"The linking wasn't completed in the required time.": "Az összekötés az elvárt időn belül nem fejeződött be.",
"Sign in new device": "Új eszköz bejelentkeztetése",
"Show QR code": "QR kód beolvasása",
"Sign in with QR code": "Belépés QR kóddal",
"Browser": "Böngésző",
"Yes, stop broadcast": "Igen, közvetítés megállítása"
}

View file

@ -2267,8 +2267,8 @@
"Value": "Valore",
"Setting ID": "ID impostazione",
"Show chat effects (animations when receiving e.g. confetti)": "Mostra effetti chat (animazioni quando si ricevono ad es. coriandoli)",
"Original event source": "Fonte dell'evento originale",
"Decrypted event source": "Fonte dell'evento decifrato",
"Original event source": "Sorgente dell'evento originale",
"Decrypted event source": "Sorgente dell'evento decifrato",
"Inviting...": "Invito...",
"Invite by username": "Invita per nome utente",
"Invite your teammates": "Invita la tua squadra",
@ -3635,5 +3635,8 @@
"stop voice broadcast": "ferma broadcast voce",
"Yes, stop broadcast": "Sì, ferma il broadcast",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Vuoi davvero fermare il tuo broadcast in diretta? Verrà terminato il broadcast e la registrazione completa sarà disponibile nella stanza.",
"Stop live broadcasting?": "Fermare il broadcast in diretta?"
"Stop live broadcasting?": "Fermare il broadcast in diretta?",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Non hai l'autorizzazione necessaria per iniziare un broadcast vocale in questa stanza. Contatta un amministratore della stanza per aggiornare le tue autorizzazioni.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Stai già registrando un broadcast vocale. Termina quello in corso per iniziarne uno nuovo.",
"Can't start a new voice broadcast": "Impossibile iniziare un nuovo broadcast vocale"
}

View file

@ -3632,5 +3632,37 @@
"stop voice broadcast": "zastaviť hlasové vysielanie",
"resume voice broadcast": "obnoviť hlasové vysielanie",
"pause voice broadcast": "pozastaviť hlasové vysielanie",
"Notifications silenced": "Oznámenia stlmené"
"Notifications silenced": "Oznámenia stlmené",
"Completing set up of your new device": "Dokončenie nastavenia nového zariadenia",
"Waiting for device to sign in": "Čaká sa na prihlásenie zariadenia",
"Connecting...": "Pripájanie…",
"Review and approve the sign in": "Skontrolujte a schváľte prihlásenie",
"Select 'Scan QR code'": "Vyberte možnosť \"Skenovať QR kód\"",
"Start at the sign in screen": "Začnite na prihlasovacej obrazovke",
"Scan the QR code below with your device that's signed out.": "Naskenujte nižšie uvedený QR kód pomocou zariadenia, ktoré je odhlásené.",
"By approving access for this device, it will have full access to your account.": "Schválením prístupu pre toto zariadenie bude mať plný prístup k vášmu účtu.",
"Check that the code below matches with your other device:": "Skontrolujte, či sa nižšie uvedený kód zhoduje s vaším druhým zariadením:",
"Devices connected": "Zariadenia pripojené",
"The homeserver doesn't support signing in another device.": "Domovský server nepodporuje prihlasovanie do iného zariadenia.",
"An unexpected error occurred.": "Vyskytla sa neočakávaná chyba.",
"The request was cancelled.": "Žiadosť bola zrušená.",
"The other device isn't signed in.": "Druhé zariadenie nie je prihlásené.",
"The other device is already signed in.": "Druhé zariadenie je už prihlásené.",
"The request was declined on the other device.": "Žiadosť bola na druhom zariadení zamietnutá.",
"Linking with this device is not supported.": "Prepojenie s týmto zariadením nie je podporované.",
"The scanned code is invalid.": "Naskenovaný kód je neplatný.",
"The linking wasn't completed in the required time.": "Prepojenie nebolo dokončené v požadovanom čase.",
"Sign in new device": "Prihlásiť nové zariadenie",
"Show QR code": "Zobraziť QR kód",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Toto zariadenie môžete použiť na prihlásenie nového zariadenia pomocou QR kódu. QR kód zobrazený na tomto zariadení musíte naskenovať pomocou zariadenia, ktoré je odhlásené.",
"Sign in with QR code": "Prihlásiť sa pomocou QR kódu",
"Browser": "Prehliadač",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Umožniť zobrazenie QR kódu v správcovi relácií na prihlásenie do iného zariadenia (vyžaduje kompatibilný domovský server)",
"Yes, stop broadcast": "Áno, zastaviť vysielanie",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Určite chcete zastaviť vaše vysielanie naživo? Tým sa vysielanie ukončí a v miestnosti bude k dispozícii celý záznam.",
"Stop live broadcasting?": "Zastaviť vysielanie naživo?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Niekto iný už nahráva hlasové vysielanie. Počkajte, kým sa skončí jeho hlasové vysielanie, a potom spustite nové.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Nemáte požadované oprávnenia na spustenie hlasového vysielania v tejto miestnosti. Obráťte sa na správcu miestnosti, aby vám rozšíril oprávnenia.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Už nahrávate hlasové vysielanie. Ukončite aktuálne hlasové vysielanie a spustite nové.",
"Can't start a new voice broadcast": "Nemôžete spustiť nové hlasové vysielanie"
}

View file

@ -3632,5 +3632,37 @@
"pause voice broadcast": "призупинити голосове мовлення",
"You have already joined this call from another device": "Ви вже приєдналися до цього виклику з іншого пристрою",
"stop voice broadcast": "припинити голосове мовлення",
"Notifications silenced": "Сповіщення стишено"
"Notifications silenced": "Сповіщення стишено",
"Sign in with QR code": "Увійти за допомогою QR-коду",
"Browser": "Браузер",
"Allow a QR code to be shown in session manager to sign in another device (requires compatible homeserver)": "Дозволити показ QR-коду в менеджері сеансів для входу на іншому пристрої (потрібен сумісний домашній сервер)",
"Yes, stop broadcast": "Так, припинити мовлення",
"Are you sure you want to stop your live broadcast?This will end the broadcast and the full recording will be available in the room.": "Ви впевнені, що хочете припинити голосове мовлення? На цьому трансляція завершиться, і повний запис буде доступний у кімнаті.",
"Stop live broadcasting?": "Припинити голосове мовлення?",
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Хтось інший вже записує голосову трансляцію. Зачекайте, поки вона завершиться, щоб почати нову.",
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "Ви не маєте необхідних дозволів для початку голосового мовлення в цій кімнаті. Зверніться до адміністратора кімнати, щоб оновити ваші дозволи.",
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "Ви вже записуєте голосову трансляцію. Завершіть поточну трансляцію, щоб розпочати нову.",
"Can't start a new voice broadcast": "Не вдалося розпочати нову голосове мовлення",
"Completing set up of your new device": "Завершення налаштування нового пристрою",
"Waiting for device to sign in": "Очікування входу з пристрою",
"Connecting...": "З'єднання...",
"Review and approve the sign in": "Розглянути та схвалити вхід",
"Select 'Scan QR code'": "Виберіть «Сканувати QR-код»",
"Start at the sign in screen": "Почніть з екрана входу",
"Scan the QR code below with your device that's signed out.": "Скануйте QR-код знизу своїм пристроєм, на якому ви вийшли.",
"By approving access for this device, it will have full access to your account.": "Затвердивши доступ для цього пристрою, ви надасте йому повний доступ до вашого облікового запису.",
"Check that the code below matches with your other device:": "Перевірте, чи збігається наведений внизу код з кодом на вашому іншому пристрої:",
"Devices connected": "Пристрої під'єднано",
"The homeserver doesn't support signing in another device.": "Домашній сервер не підтримує вхід на іншому пристрої.",
"An unexpected error occurred.": "Виникла непередбачувана помилка.",
"The request was cancelled.": "Запит було скасовано.",
"The other device isn't signed in.": "На іншому пристрої вхід не виконано.",
"The other device is already signed in.": "На іншому пристрої вхід було виконано.",
"The request was declined on the other device.": "На іншому пристрої запит відхилено.",
"Linking with this device is not supported.": "Зв'язок з цим пристроєм не підтримується.",
"The scanned code is invalid.": "Сканований код недійсний.",
"The linking wasn't completed in the required time.": "У встановлені терміни з'єднання не було виконано.",
"Sign in new device": "Увійти на новому пристрої",
"Show QR code": "Показати QR-код",
"You can use this device to sign in a new device with a QR code. You will need to scan the QR code shown on this device with your device that's signed out.": "Ви можете використовувати цей пристрій для входу на новому пристрої за допомогою QR-коду. Вам потрібно буде сканувати QR-код, показаний на цьому пристрої, своїм пристроєм, на якому ви вийшли."
}

View file

@ -43,6 +43,8 @@ import { WidgetMessagingStore, WidgetMessagingStoreEvent } from "../stores/widge
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../stores/ActiveWidgetStore";
import PlatformPeg from "../PlatformPeg";
import { getCurrentLanguage } from "../languageHandler";
import DesktopCapturerSourcePicker from "../components/views/elements/DesktopCapturerSourcePicker";
import Modal from "../Modal";
const TIMEOUT_MS = 16000;
@ -639,10 +641,6 @@ export class ElementCall extends Call {
baseUrl: client.baseUrl,
lang: getCurrentLanguage().replace("_", "-"),
});
// Currently, the screen-sharing support is the same is it is for Jitsi
if (!PlatformPeg.get().supportsJitsiScreensharing()) {
params.append("hideScreensharing", "");
}
url.hash = `#?${params.toString()}`;
// To use Element Call without touching room state, we create a virtual
@ -818,6 +816,7 @@ 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);
this.messaging!.on(`action:${ElementWidgetActions.Screenshare}`, this.onScreenshare);
}
protected async performDisconnection(): Promise<void> {
@ -831,8 +830,9 @@ export class ElementCall extends Call {
public setDisconnected() {
this.client.off(ClientEvent.ToDeviceEvent, this.onToDeviceEvent);
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
this.messaging!.off(`action:${ElementWidgetActions.Screenshare}`, this.onSpotlightLayout);
super.setDisconnected();
}
@ -951,4 +951,20 @@ export class ElementCall extends Call {
this.layout = Layout.Spotlight;
await this.messaging!.transport.reply(ev.detail, {}); // ack
};
private onScreenshare = async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
if (PlatformPeg.get().supportsDesktopCapturer()) {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
await this.messaging!.transport.reply(ev.detail, {
failed: !source,
desktopCapturerSourceId: source,
});
} else {
await this.messaging!.transport.reply(ev.detail, {});
}
};
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixClientPeg } from "../MatrixClientPeg";
import { SdkContextClass } from "../contexts/SDKContext";
import SettingsStore from "../settings/SettingsStore";
import { isLocalRoom } from "../utils/localRoom/isLocalRoom";
import Timer from "../utils/Timer";
@ -34,17 +34,10 @@ export default class TypingStore {
};
};
constructor() {
constructor(private readonly context: SdkContextClass) {
this.reset();
}
public static sharedInstance(): TypingStore {
if (window.mxTypingStore === undefined) {
window.mxTypingStore = new TypingStore();
}
return window.mxTypingStore;
}
/**
* Clears all cached typing states. Intended to be called when the
* MatrixClientPeg client changes.
@ -108,6 +101,6 @@ export default class TypingStore {
} else currentTyping.userTimer.restart();
}
MatrixClientPeg.get().sendTyping(roomId, isTyping, TYPING_SERVER_TIMEOUT);
this.context.client?.sendTyping(roomId, isTyping, TYPING_SERVER_TIMEOUT);
}
}

View file

@ -29,6 +29,7 @@ export enum ElementWidgetActions {
// Actions for switching layouts
TileLayout = "io.element.tile_layout",
SpotlightLayout = "io.element.spotlight_layout",
Screenshare = "io.element.screenshare",
OpenIntegrationManager = "integration_manager_open",

View file

@ -47,7 +47,7 @@ import Modal from "../../Modal";
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
import WidgetCapabilitiesPromptDialog from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
import { WidgetPermissionCustomisations } from "../../customisations/WidgetPermissions";
import { OIDCState, WidgetPermissionStore } from "./WidgetPermissionStore";
import { OIDCState } from "./WidgetPermissionStore";
import { WidgetType } from "../../widgets/WidgetType";
import { CHAT_EFFECTS } from "../../effects";
import { containsEmoji } from "../../effects/utils";
@ -350,7 +350,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
}
public async askOpenID(observer: SimpleObservable<IOpenIDUpdate>) {
const oidcState = WidgetPermissionStore.instance.getOIDCState(
const oidcState = SdkContextClass.instance.widgetPermissionStore.getOIDCState(
this.forWidget, this.forWidgetKind, this.inRoomId,
);

View file

@ -17,8 +17,8 @@
import { Widget, WidgetKind } from "matrix-widget-api";
import SettingsStore from "../../settings/SettingsStore";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { SettingLevel } from "../../settings/SettingLevel";
import { SdkContextClass } from "../../contexts/SDKContext";
export enum OIDCState {
Allowed, // user has set the remembered value as allowed
@ -27,16 +27,7 @@ export enum OIDCState {
}
export class WidgetPermissionStore {
private static internalInstance: WidgetPermissionStore;
private constructor() {
}
public static get instance(): WidgetPermissionStore {
if (!WidgetPermissionStore.internalInstance) {
WidgetPermissionStore.internalInstance = new WidgetPermissionStore();
}
return WidgetPermissionStore.internalInstance;
public constructor(private readonly context: SdkContextClass) {
}
// TODO (all functions here): Merge widgetKind with the widget definition
@ -44,7 +35,7 @@ export class WidgetPermissionStore {
private packSettingKey(widget: Widget, kind: WidgetKind, roomId?: string): string {
let location = roomId;
if (kind !== WidgetKind.Room) {
location = MatrixClientPeg.get().getUserId();
location = this.context.client?.getUserId();
}
if (kind === WidgetKind.Modal) {
location = '*MODAL*-' + location; // to guarantee differentiation from whatever spawned it
@ -71,7 +62,10 @@ export class WidgetPermissionStore {
public setOIDCState(widget: Widget, kind: WidgetKind, roomId: string, newState: OIDCState) {
const settingsKey = this.packSettingKey(widget, kind, roomId);
const currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
let currentValues = SettingsStore.getValue("widgetOpenIDPermissions");
if (!currentValues) {
currentValues = {};
}
if (!currentValues.allow) currentValues.allow = [];
if (!currentValues.deny) currentValues.deny = [];

View file

@ -17,10 +17,11 @@ limitations under the License.
import { Optional } from "matrix-events-sdk";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import { getChunkLength } from "..";
import { VoiceRecording } from "../../audio/VoiceRecording";
import SdkConfig, { DEFAULTS } from "../../SdkConfig";
import { concat } from "../../utils/arrays";
import { IDestroyable } from "../../utils/IDestroyable";
import { Singleflight } from "../../utils/Singleflight";
export enum VoiceBroadcastRecorderEvent {
ChunkRecorded = "chunk_recorded",
@ -65,6 +66,8 @@ export class VoiceBroadcastRecorder
*/
public async stop(): Promise<Optional<ChunkRecordedPayload>> {
await this.voiceRecording.stop();
// forget about that call, so that we can stop it again later
Singleflight.forgetAllFor(this.voiceRecording);
return this.extractChunk();
}
@ -136,6 +139,5 @@ export class VoiceBroadcastRecorder
}
export const createVoiceBroadcastRecorder = (): VoiceBroadcastRecorder => {
const targetChunkLength = SdkConfig.get("voice_broadcast")?.chunk_length || DEFAULTS.voice_broadcast!.chunk_length;
return new VoiceBroadcastRecorder(new VoiceRecording(), targetChunkLength);
return new VoiceBroadcastRecorder(new VoiceRecording(), getChunkLength());
};

View file

@ -49,6 +49,7 @@ export const VoiceBroadcastBody: React.FC<IBodyProps> = ({ mxEvent }) => {
client,
);
relationsHelper.on(RelationsHelperEvent.Add, onInfoEvent);
relationsHelper.emitCurrent();
return () => {
relationsHelper.destroy();

View file

@ -16,12 +16,12 @@ limitations under the License.
import React from "react";
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
import { _t } from "../../../languageHandler";
export const LiveBadge: React.FC = () => {
return <div className="mx_LiveBadge">
<Icon type={IconType.Live} colour={IconColour.LiveBadge} />
<LiveIcon className="mx_Icon mx_Icon_16" />
{ _t("Live") }
</div>;
};

View file

@ -1,53 +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 { VoiceBroadcastPlaybackState } from "../..";
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import { _t } from "../../../languageHandler";
const stateIconMap = new Map([
[VoiceBroadcastPlaybackState.Playing, IconType.Pause],
[VoiceBroadcastPlaybackState.Paused, IconType.Play],
[VoiceBroadcastPlaybackState.Stopped, IconType.Play],
]);
interface Props {
onClick: () => void;
state: VoiceBroadcastPlaybackState;
}
export const PlaybackControlButton: React.FC<Props> = ({
onClick,
state,
}) => {
const ariaLabel = state === VoiceBroadcastPlaybackState.Playing
? _t("pause voice broadcast")
: _t("resume voice broadcast");
return <AccessibleButton
className="mx_BroadcastPlaybackControlButton"
onClick={onClick}
aria-label={ariaLabel}
>
<Icon
colour={IconColour.CompoundSecondaryContent}
type={stateIconMap.get(state)}
/>
</AccessibleButton>;
};

View file

@ -14,27 +14,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import classNames from "classnames";
import React from "react";
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import { _t } from "../../../languageHandler";
interface Props {
className?: string;
icon: React.FC<React.SVGProps<SVGSVGElement>>;
label: string;
onClick: () => void;
}
export const StopButton: React.FC<Props> = ({
export const VoiceBroadcastControl: React.FC<Props> = ({
className = "",
icon: Icon,
label,
onClick,
}) => {
return <AccessibleButton
className="mx_BroadcastPlaybackControlButton"
className={classNames("mx_VoiceBroadcastControl", className)}
onClick={onClick}
aria-label={_t("stop voice broadcast")}
aria-label={label}
>
<Icon
colour={IconColour.CompoundSecondaryContent}
type={IconType.Stop}
/>
<Icon className="mx_Icon mx_Icon_16" />
</AccessibleButton>;
};

View file

@ -15,7 +15,8 @@ import React from "react";
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { LiveBadge } from "../..";
import { Icon, IconColour, IconType } from "../../../components/atoms/Icon";
import { Icon as LiveIcon } from "../../../../res/img/element-icons/live.svg";
import { Icon as MicrophoneIcon } from "../../../../res/img/voip/call-view/mic-on.svg";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../../../components/views/avatars/RoomAvatar";
@ -34,7 +35,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
}) => {
const broadcast = showBroadcast
? <div className="mx_VoiceBroadcastHeader_line">
<Icon type={IconType.Live} colour={IconColour.CompoundSecondaryContent} />
<LiveIcon className="mx_Icon mx_Icon_16" />
{ _t("Voice broadcast") }
</div>
: null;
@ -46,7 +47,7 @@ export const VoiceBroadcastHeader: React.FC<VoiceBroadcastHeaderProps> = ({
{ room.name }
</div>
<div className="mx_VoiceBroadcastHeader_line">
<Icon type={IconType.Microphone} colour={IconColour.CompoundSecondaryContent} />
<MicrophoneIcon className="mx_Icon mx_Icon_16" />
<span>{ sender.name }</span>
</div>
{ broadcast }

View file

@ -17,13 +17,16 @@ limitations under the License.
import React from "react";
import {
PlaybackControlButton,
VoiceBroadcastControl,
VoiceBroadcastHeader,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackState,
} from "../..";
import Spinner from "../../../components/views/elements/Spinner";
import { useVoiceBroadcastPlayback } from "../../hooks/useVoiceBroadcastPlayback";
import { Icon as PlayIcon } from "../../../../res/img/element-icons/play.svg";
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
import { _t } from "../../../languageHandler";
interface VoiceBroadcastPlaybackBodyProps {
playback: VoiceBroadcastPlayback;
@ -40,9 +43,35 @@ export const VoiceBroadcastPlaybackBody: React.FC<VoiceBroadcastPlaybackBodyProp
playbackState,
} = useVoiceBroadcastPlayback(playback);
const control = playbackState === VoiceBroadcastPlaybackState.Buffering
? <Spinner />
: <PlaybackControlButton onClick={toggle} state={playbackState} />;
let control: React.ReactNode;
if (playbackState === VoiceBroadcastPlaybackState.Buffering) {
control = <Spinner />;
} else {
let controlIcon: React.FC<React.SVGProps<SVGSVGElement>>;
let controlLabel: string;
switch (playbackState) {
case VoiceBroadcastPlaybackState.Stopped:
controlIcon = PlayIcon;
controlLabel = _t("play voice broadcast");
break;
case VoiceBroadcastPlaybackState.Paused:
controlIcon = PlayIcon;
controlLabel = _t("resume voice broadcast");
break;
case VoiceBroadcastPlaybackState.Playing:
controlIcon = PauseIcon;
controlLabel = _t("pause voice broadcast");
break;
}
control = <VoiceBroadcastControl
label={controlLabel}
icon={controlIcon}
onClick={toggle}
/>;
}
return (
<div className="mx_VoiceBroadcastPlaybackBody">

View file

@ -17,11 +17,16 @@ limitations under the License.
import React from "react";
import {
StopButton,
VoiceBroadcastControl,
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
} from "../..";
import { useVoiceBroadcastRecording } from "../../hooks/useVoiceBroadcastRecording";
import { VoiceBroadcastHeader } from "../atoms/VoiceBroadcastHeader";
import { Icon as StopIcon } from "../../../../res/img/element-icons/Stop.svg";
import { Icon as PauseIcon } from "../../../../res/img/element-icons/pause.svg";
import { Icon as RecordIcon } from "../../../../res/img/element-icons/Record.svg";
import { _t } from "../../../languageHandler";
interface VoiceBroadcastRecordingPipProps {
recording: VoiceBroadcastRecording;
@ -30,11 +35,22 @@ interface VoiceBroadcastRecordingPipProps {
export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProps> = ({ recording }) => {
const {
live,
sender,
recordingState,
room,
sender,
stopRecording,
toggleRecording,
} = useVoiceBroadcastRecording(recording);
const toggleControl = recordingState === VoiceBroadcastInfoState.Paused
? <VoiceBroadcastControl
className="mx_VoiceBroadcastControl-recording"
onClick={toggleRecording}
icon={RecordIcon}
label={_t("resume voice broadcast")}
/>
: <VoiceBroadcastControl onClick={toggleRecording} icon={PauseIcon} label={_t("pause voice broadcast")} />;
return <div
className="mx_VoiceBroadcastRecordingPip"
>
@ -45,7 +61,12 @@ export const VoiceBroadcastRecordingPip: React.FC<VoiceBroadcastRecordingPipProp
/>
<hr className="mx_VoiceBroadcastRecordingPip_divider" />
<div className="mx_VoiceBroadcastRecordingPip_controls">
<StopButton onClick={stopRecording} />
{ toggleControl }
<VoiceBroadcastControl
icon={StopIcon}
label="Stop Recording"
onClick={stopRecording}
/>
</div>
</div>;
};

View file

@ -20,7 +20,6 @@ import {
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingEvent,
VoiceBroadcastRecordingsStore,
} from "..";
import QuestionDialog from "../../components/views/dialogs/QuestionDialog";
import { useTypedEventEmitter } from "../../hooks/useEventEmitter";
@ -53,24 +52,31 @@ export const useVoiceBroadcastRecording = (recording: VoiceBroadcastRecording) =
const confirmed = await showStopBroadcastingDialog();
if (confirmed) {
recording.stop();
VoiceBroadcastRecordingsStore.instance().clearCurrent();
await recording.stop();
}
};
const [live, setLive] = useState(recording.getState() === VoiceBroadcastInfoState.Started);
const [recordingState, setRecordingState] = useState(recording.getState());
useTypedEventEmitter(
recording,
VoiceBroadcastRecordingEvent.StateChanged,
(state: VoiceBroadcastInfoState, _recording: VoiceBroadcastRecording) => {
setLive(state === VoiceBroadcastInfoState.Started);
setRecordingState(state);
},
);
const live = [
VoiceBroadcastInfoState.Started,
VoiceBroadcastInfoState.Paused,
VoiceBroadcastInfoState.Running,
].includes(recordingState);
return {
live,
recordingState,
room,
sender: recording.infoEvent.sender,
stopRecording,
toggleRecording: recording.toggle,
};
};

View file

@ -26,8 +26,7 @@ export * from "./models/VoiceBroadcastRecording";
export * from "./audio/VoiceBroadcastRecorder";
export * from "./components/VoiceBroadcastBody";
export * from "./components/atoms/LiveBadge";
export * from "./components/atoms/PlaybackControlButton";
export * from "./components/atoms/StopButton";
export * from "./components/atoms/VoiceBroadcastControl";
export * from "./components/atoms/VoiceBroadcastHeader";
export * from "./components/molecules/VoiceBroadcastPlaybackBody";
export * from "./components/molecules/VoiceBroadcastRecordingBody";
@ -35,10 +34,14 @@ export * from "./components/molecules/VoiceBroadcastRecordingPip";
export * from "./hooks/useVoiceBroadcastRecording";
export * from "./stores/VoiceBroadcastPlaybacksStore";
export * from "./stores/VoiceBroadcastRecordingsStore";
export * from "./utils/getChunkLength";
export * from "./utils/hasRoomLiveVoiceBroadcast";
export * from "./utils/findRoomLiveVoiceBroadcastFromUserAndDevice";
export * from "./utils/resumeVoiceBroadcastInRoom";
export * from "./utils/shouldDisplayAsVoiceBroadcastRecordingTile";
export * from "./utils/shouldDisplayAsVoiceBroadcastTile";
export * from "./utils/startNewVoiceBroadcastRecording";
export * from "./utils/VoiceBroadcastResumer";
export const VoiceBroadcastInfoEventType = "io.element.voice_broadcast_info";
export const VoiceBroadcastChunkEventType = "io.element.voice_broadcast_chunk";

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, MatrixEventEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import {
@ -52,9 +52,23 @@ export class VoiceBroadcastRecording
public constructor(
public readonly infoEvent: MatrixEvent,
private client: MatrixClient,
initialState?: VoiceBroadcastInfoState,
) {
super();
if (initialState) {
this.state = initialState;
} else {
this.setInitialStateFromInfoEvent();
}
// TODO Michael W: listen for state updates
//
this.infoEvent.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.dispatcherRef = dis.register(this.onAction);
}
private setInitialStateFromInfoEvent(): void {
const room = this.client.getRoom(this.infoEvent.getRoomId());
const relations = room?.getUnfilteredTimelineSet()?.relations?.getChildEventsForEvent(
this.infoEvent.getId(),
@ -65,9 +79,6 @@ export class VoiceBroadcastRecording
this.state = !relatedEvents?.find((event: MatrixEvent) => {
return event.getContent()?.state === VoiceBroadcastInfoState.Stopped;
}) ? VoiceBroadcastInfoState.Started : VoiceBroadcastInfoState.Stopped;
// TODO Michael W: add listening for updates
this.dispatcherRef = dis.register(this.onAction);
}
public async start(): Promise<void> {
@ -75,11 +86,38 @@ export class VoiceBroadcastRecording
}
public async stop(): Promise<void> {
if (this.state === VoiceBroadcastInfoState.Stopped) return;
this.setState(VoiceBroadcastInfoState.Stopped);
await this.stopRecorder();
await this.sendStoppedStateEvent();
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Stopped);
}
public async pause(): Promise<void> {
// stopped or already paused recordings cannot be paused
if ([VoiceBroadcastInfoState.Stopped, VoiceBroadcastInfoState.Paused].includes(this.state)) return;
this.setState(VoiceBroadcastInfoState.Paused);
await this.stopRecorder();
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Paused);
}
public async resume(): Promise<void> {
if (this.state !== VoiceBroadcastInfoState.Paused) return;
this.setState(VoiceBroadcastInfoState.Running);
await this.getRecorder().start();
await this.sendInfoStateEvent(VoiceBroadcastInfoState.Running);
}
public toggle = async (): Promise<void> => {
if (this.getState() === VoiceBroadcastInfoState.Paused) return this.resume();
if ([VoiceBroadcastInfoState.Started, VoiceBroadcastInfoState.Running].includes(this.getState())) {
return this.pause();
}
};
public getState(): VoiceBroadcastInfoState {
return this.state;
}
@ -99,10 +137,19 @@ export class VoiceBroadcastRecording
this.recorder.stop();
}
this.infoEvent.off(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
this.removeAllListeners();
dis.unregister(this.dispatcherRef);
}
private onBeforeRedaction = () => {
if (this.getState() !== VoiceBroadcastInfoState.Stopped) {
this.setState(VoiceBroadcastInfoState.Stopped);
// destroy cleans up everything
this.destroy();
}
};
private onAction = (payload: ActionPayload) => {
if (payload.action !== "call_state") return;
@ -152,14 +199,14 @@ export class VoiceBroadcastRecording
await this.client.sendMessage(this.infoEvent.getRoomId(), content);
}
private async sendStoppedStateEvent(): Promise<void> {
private async sendInfoStateEvent(state: VoiceBroadcastInfoState): Promise<void> {
// TODO Michael W: add error handling for state event
await this.client.sendStateEvent(
this.infoEvent.getRoomId(),
VoiceBroadcastInfoEventType,
{
device_id: this.client.getDeviceId(),
state: VoiceBroadcastInfoState.Stopped,
state,
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: this.infoEvent.getId(),

View file

@ -17,7 +17,7 @@ limitations under the License.
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { TypedEventEmitter } from "matrix-js-sdk/src/models/typed-event-emitter";
import { VoiceBroadcastRecording } from "..";
import { VoiceBroadcastInfoState, VoiceBroadcastRecording, VoiceBroadcastRecordingEvent } from "..";
export enum VoiceBroadcastRecordingsStoreEvent {
CurrentChanged = "current_changed",
@ -41,7 +41,12 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
public setCurrent(current: VoiceBroadcastRecording): void {
if (this.current === current) return;
if (this.current) {
this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
}
this.current = current;
this.current.on(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
this.recordings.set(current.infoEvent.getId(), current);
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, current);
}
@ -51,8 +56,9 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
}
public clearCurrent(): void {
if (this.current === null) return;
if (!this.current) return;
this.current.off(VoiceBroadcastRecordingEvent.StateChanged, this.onCurrentStateChanged);
this.current = null;
this.emit(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, null);
}
@ -67,6 +73,12 @@ export class VoiceBroadcastRecordingsStore extends TypedEventEmitter<VoiceBroadc
return this.recordings.get(infoEventId);
}
private onCurrentStateChanged = (state: VoiceBroadcastInfoState) => {
if (state === VoiceBroadcastInfoState.Stopped) {
this.clearCurrent();
}
};
private static readonly cachedInstance = new VoiceBroadcastRecordingsStore();
/**

View file

@ -0,0 +1,56 @@
/*
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 { ClientEvent, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { IDestroyable } from "../../utils/IDestroyable";
import { findRoomLiveVoiceBroadcastFromUserAndDevice } from "./findRoomLiveVoiceBroadcastFromUserAndDevice";
import { resumeVoiceBroadcastInRoom } from "./resumeVoiceBroadcastInRoom";
export class VoiceBroadcastResumer implements IDestroyable {
private seenRooms = new Set<string>();
private userId: string;
private deviceId: string;
public constructor(
private client: MatrixClient,
) {
this.client.on(ClientEvent.Room, this.onRoom);
this.userId = this.client.getUserId();
this.deviceId = this.client.getDeviceId();
}
private onRoom = (room: Room): void => {
if (this.seenRooms.has(room.roomId)) return;
this.seenRooms.add(room.roomId);
const infoEvent = findRoomLiveVoiceBroadcastFromUserAndDevice(
room,
this.userId,
this.deviceId,
);
if (infoEvent) {
resumeVoiceBroadcastInRoom(infoEvent, room, this.client);
}
};
destroy(): void {
this.client.off(ClientEvent.Room, this.onRoom);
this.seenRooms = new Set<string>();
}
}

View file

@ -0,0 +1,37 @@
/*
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 { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "..";
export const findRoomLiveVoiceBroadcastFromUserAndDevice = (
room: Room,
userId: string,
deviceId: string,
): MatrixEvent | null => {
const stateEvent = room.currentState.getStateEvents(VoiceBroadcastInfoEventType, userId);
// no broadcast from that user
if (!stateEvent) return null;
const content = stateEvent.getContent() || {};
// stopped broadcast
if (content.state === VoiceBroadcastInfoState.Stopped) return null;
return content.device_id === deviceId ? stateEvent : null;
};

View file

@ -0,0 +1,29 @@
/*
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 SdkConfig, { DEFAULTS } from "../../SdkConfig";
/**
* Returns the target chunk length for voice broadcasts:
* - Tries to get the value from the voice_broadcast.chunk_length config
* - If that fails from DEFAULTS
* - If that fails fall back to 120 (two minutes)
*/
export const getChunkLength = (): number => {
return SdkConfig.get("voice_broadcast")?.chunk_length
|| DEFAULTS.voice_broadcast?.chunk_length
|| 120;
};

View file

@ -0,0 +1,34 @@
/*
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 { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "..";
import { VoiceBroadcastRecordingsStore } from "../stores/VoiceBroadcastRecordingsStore";
export const resumeVoiceBroadcastInRoom = (latestInfoEvent: MatrixEvent, room: Room, client: MatrixClient) => {
// voice broadcasts are based on their started event, try to find it
const infoEvent = latestInfoEvent.getContent()?.state === VoiceBroadcastInfoState.Started
? latestInfoEvent
: room.findEventById(latestInfoEvent.getRelation()?.event_id);
if (!infoEvent) {
return;
}
const recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Paused);
VoiceBroadcastRecordingsStore.instance().setCurrent(recording);
};

View file

@ -28,6 +28,7 @@ import {
VoiceBroadcastRecordingsStore,
VoiceBroadcastRecording,
hasRoomLiveVoiceBroadcast,
getChunkLength,
} from "..";
const startBroadcast = async (
@ -67,7 +68,7 @@ const startBroadcast = async (
{
device_id: client.getDeviceId(),
state: VoiceBroadcastInfoState.Started,
chunk_length: 300,
chunk_length: getChunkLength(),
} as VoiceBroadcastInfoEventContent,
client.getUserId(),
);
@ -113,6 +114,11 @@ export const startNewVoiceBroadcastRecording = async (
client: MatrixClient,
recordingsStore: VoiceBroadcastRecordingsStore,
): Promise<VoiceBroadcastRecording | null> => {
if (recordingsStore.getCurrent()) {
showAlreadyRecordingDialog();
return null;
}
const currentUserId = client.getUserId();
if (!room.currentState.maySendStateEvent(VoiceBroadcastInfoEventType, currentUserId)) {

View file

@ -22,16 +22,18 @@ import RightPanelStore from "../src/stores/right-panel/RightPanelStore";
import { RoomViewStore } from "../src/stores/RoomViewStore";
import { SpaceStoreClass } from "../src/stores/spaces/SpaceStore";
import { WidgetLayoutStore } from "../src/stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../src/stores/widgets/WidgetPermissionStore";
import WidgetStore from "../src/stores/WidgetStore";
/**
* A class which provides the same API as Stores but adds additional unsafe setters which can
* A class which provides the same API as SdkContextClass but adds additional unsafe setters which can
* replace individual stores. This is useful for tests which need to mock out stores.
*/
export class TestStores extends SdkContextClass {
export class TestSdkContext extends SdkContextClass {
public _RightPanelStore?: RightPanelStore;
public _RoomNotificationStateStore?: RoomNotificationStateStore;
public _RoomViewStore?: RoomViewStore;
public _WidgetPermissionStore?: WidgetPermissionStore;
public _WidgetLayoutStore?: WidgetLayoutStore;
public _WidgetStore?: WidgetStore;
public _PosthogAnalytics?: PosthogAnalytics;

View file

@ -1,47 +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 { render } from "@testing-library/react";
import { Icon, IconColour, IconSize, IconType } from "../../../src/components/atoms/Icon";
describe("Icon", () => {
it.each([
IconColour.Accent,
IconColour.LiveBadge,
])("should render the colour %s", (colour: IconColour) => {
const { container } = render(
<Icon
colour={colour}
type={IconType.Live}
/>,
);
expect(container).toMatchSnapshot();
});
it.each([
IconSize.S16,
])("should render the size %s", (size: IconSize) => {
const { container } = render(
<Icon
size={size}
type={IconType.Live}
/>,
);
expect(container).toMatchSnapshot();
});
});

View file

@ -1,34 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Icon should render the colour accent 1`] = `
<div>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_accent"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
`;
exports[`Icon should render the colour live-badge 1`] = `
<div>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
`;
exports[`Icon should render the size 16 1`] = `
<div>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_accent"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
`;

View file

@ -0,0 +1,158 @@
/*
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 { getByTestId, render, RenderResult, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import React, { useState } from "react";
import { act } from "react-dom/test-utils";
import ThreadView from "../../../src/components/structures/ThreadView";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import { mockPlatformPeg } from "../../test-utils/platform";
import { getRoomContext } from "../../test-utils/room";
import { stubClient } from "../../test-utils/test-utils";
import { mkThread } from "../../test-utils/threads";
describe("ThreadView", () => {
const ROOM_ID = "!roomId:example.org";
const SENDER = "@alice:example.org";
let mockClient: MatrixClient;
let room: Room;
let rootEvent: MatrixEvent;
let changeEvent: (event: MatrixEvent) => void;
function TestThreadView() {
const [event, setEvent] = useState(rootEvent);
changeEvent = setEvent;
return <MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={getRoomContext(room, {
canSendMessages: true,
})}>
<ThreadView
room={room}
onClose={jest.fn()}
mxEvent={event}
resizeNotifier={new ResizeNotifier()}
/>
</RoomContext.Provider>,
</MatrixClientContext.Provider>;
}
async function getComponent(): Promise<RenderResult> {
const renderResult = render(
<TestThreadView />,
);
await waitFor(() => {
expect(() => getByTestId(renderResult.container, 'spinner')).toThrow();
});
return renderResult;
}
async function sendMessage(container, text): Promise<void> {
const composer = getByTestId(container, "basicmessagecomposer");
await userEvent.click(composer);
await userEvent.keyboard(text);
const sendMessageBtn = getByTestId(container, "sendmessagebtn");
await userEvent.click(sendMessageBtn);
}
function expectedMessageBody(rootEvent, message) {
return {
"body": message,
"m.relates_to": {
"event_id": rootEvent.getId(),
"is_falling_back": true,
"m.in_reply_to": {
"event_id": rootEvent.getThread().lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name);
}).getId(),
},
"rel_type": RelationType.Thread,
},
"msgtype": MsgType.Text,
};
}
beforeEach(() => {
jest.clearAllMocks();
stubClient();
mockPlatformPeg();
mockClient = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const res = mkThread({
room,
client: mockClient,
authorId: mockClient.getUserId(),
participantUserIds: [mockClient.getUserId()],
});
rootEvent = res.rootEvent;
DMRoomMap.makeShared();
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(SENDER);
});
it("sends a message with the correct fallback", async () => {
const { container } = await getComponent();
await sendMessage(container, "Hello world!");
expect(mockClient.sendMessage).toHaveBeenCalledWith(
ROOM_ID, rootEvent.getId(), expectedMessageBody(rootEvent, "Hello world!"),
);
});
it("sends a message with the correct fallback", async () => {
const { container } = await getComponent();
const { rootEvent: rootEvent2 } = mkThread({
room,
client: mockClient,
authorId: mockClient.getUserId(),
participantUserIds: [mockClient.getUserId()],
});
act(() => {
changeEvent(rootEvent2);
});
await sendMessage(container, "yolo");
expect(mockClient.sendMessage).toHaveBeenCalledWith(
ROOM_ID, rootEvent2.getId(), expectedMessageBody(rootEvent2, "yolo"),
);
});
});

View file

@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\" data-testid=\\"spinner\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_RoomStatusBar mx_RoomStatusBar_unsentMessages\\"><div role=\\"alert\\"><div class=\\"mx_RoomStatusBar_unsentBadge\\"><div class=\\"mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char\\"><span class=\\"mx_NotificationBadge_count\\">!</span></div></div><div><div class=\\"mx_RoomStatusBar_unsentTitle\\">Some of your messages have not been sent</div></div><div class=\\"mx_RoomStatusBar_unsentButtonBar\\"><div role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_RoomStatusBar_unsentRetry\\">Retry</div></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" data-testid=\\"basicmessagecomposer\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" data-testid=\\"basicmessagecomposer\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;

View file

@ -0,0 +1,84 @@
/*
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 { getByTestId, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import React from "react";
import ThreadListContextMenu, {
ThreadListContextMenuProps,
} from "../../../../src/components/views/context_menus/ThreadListContextMenu";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { stubClient } from "../../../test-utils/test-utils";
import { mkThread } from "../../../test-utils/threads";
describe("ThreadListContextMenu", () => {
const ROOM_ID = "!123:matrix.org";
let room: Room;
let mockClient: MatrixClient;
let event: MatrixEvent;
function getComponent(props: Partial<ThreadListContextMenuProps>) {
return render(<ThreadListContextMenu
mxEvent={event}
{...props}
/>);
}
beforeEach(() => {
jest.clearAllMocks();
stubClient();
mockClient = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const res = mkThread({
room,
client: mockClient,
authorId: mockClient.getUserId(),
participantUserIds: [mockClient.getUserId()],
});
event = res.rootEvent;
});
it("does not render the permalink", async () => {
const { container } = getComponent({});
const btn = getByTestId(container, "threadlist-dropdown-button");
await userEvent.click(btn);
expect(screen.queryByTestId("copy-thread-link")).toBeNull();
});
it("does render the permalink", async () => {
const { container } = getComponent({
permalinkCreator: new RoomPermalinkCreator(room, room.roomId, false),
});
const btn = getByTestId(container, "threadlist-dropdown-button");
await userEvent.click(btn);
expect(screen.queryByTestId("copy-thread-link")).not.toBeNull();
});
});

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import { act } from "react-dom/test-utils";
import { sleep } from "matrix-js-sdk/src/utils";
import { ISendEventResponse, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix";
import { ISendEventResponse, MatrixClient, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
// eslint-disable-next-line deprecate/import
import { mount } from 'enzyme';
import { mocked } from "jest-mock";
@ -291,7 +291,7 @@ describe('<SendMessageComposer/>', () => {
it('correctly sets the editorStateKey for threads', () => {
const relation = {
rel_type: "m.thread",
rel_type: RelationType.Thread,
event_id: "myFakeThreadId",
};
const includeReplyLegacyFallback = false;

View file

@ -20,13 +20,12 @@ import { act, render, screen, waitFor } from "@testing-library/react";
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import RoomContext from "../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
import SettingsStore from "../../../../../src/settings/SettingsStore";
// Work around missing ClipboardEvent type
@ -74,43 +73,7 @@ describe('WysiwygComposer', () => {
return eventId === mockEvent.getId() ? mockEvent : null;
});
const defaultRoomContext: IRoomState = {
room: mockRoom,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
};
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
let sendMessage: () => void;
const customRender = (onChange = (_content: string) => void 0, disabled = false) => {

View file

@ -31,6 +31,7 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
<div
aria-label="Loading..."
className="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style={
Object {

View file

@ -4,6 +4,7 @@ exports[`<LoginWithQR /> approves login and waits for new device 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -32,6 +33,7 @@ exports[`<LoginWithQR /> approves login and waits for new device 1`] = `
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
@ -61,6 +63,7 @@ exports[`<LoginWithQR /> displays confirmation digits after connected to rendezv
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -122,6 +125,7 @@ exports[`<LoginWithQR /> displays error when approving login fails 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
@ -168,6 +172,7 @@ exports[`<LoginWithQR /> displays qr code after it is created 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -214,6 +219,7 @@ exports[`<LoginWithQR /> displays qr code after it is created 1`] = `
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
@ -232,6 +238,7 @@ exports[`<LoginWithQR /> displays unknown error if connection to rendezvous fail
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
@ -278,6 +285,7 @@ exports[`<LoginWithQR /> no content in case of no support 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
@ -324,6 +332,7 @@ exports[`<LoginWithQR /> renders spinner while generating code 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -352,6 +361,7 @@ exports[`<LoginWithQR /> renders spinner while generating code 1`] = `
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>

View file

@ -13,7 +13,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { render } from '@testing-library/react';
import { fireEvent, render } from '@testing-library/react';
import React from 'react';
import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab";
@ -26,6 +26,7 @@ import {
mockClientMethodsCrypto,
mockClientMethodsDevice,
mockPlatformPeg,
flushPromises,
} from '../../../../../test-utils';
describe('<SecurityUserSettingsTab />', () => {
@ -42,6 +43,12 @@ describe('<SecurityUserSettingsTab />', () => {
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getVersions: jest.fn().mockResolvedValue({
unstable_features: {
'org.matrix.msc3882': true,
'org.matrix.msc3886': true,
},
}),
});
const getComponent = () =>
@ -70,4 +77,34 @@ describe('<SecurityUserSettingsTab />', () => {
expect(queryByTestId('devices-section')).toBeFalsy();
});
it('does not render qr code login section when disabled', () => {
settingsValueSpy.mockReturnValue(false);
const { queryByText } = render(getComponent());
expect(settingsValueSpy).toHaveBeenCalledWith('feature_qr_signin_reciprocate_show');
expect(queryByText('Sign in with QR code')).toBeFalsy();
});
it('renders qr code login section when enabled', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText } = render(getComponent());
// wait for versions call to settle
await flushPromises();
expect(getByText('Sign in with QR code')).toBeTruthy();
});
it('enters qr code login section when show QR code button clicked', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText, getByTestId } = render(getComponent());
// wait for versions call to settle
await flushPromises();
fireEvent.click(getByText('Show QR code'));
expect(getByTestId("login-with-qr")).toBeTruthy();
});
});

View file

@ -34,6 +34,7 @@ import {
import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab';
import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext';
import {
flushPromises,
flushPromisesWithFakeTimers,
getMockClientWithEventEmitter,
mkPusher,
@ -47,6 +48,7 @@ import {
ExtendedDevice,
} from '../../../../../../src/components/views/settings/devices/types';
import { INACTIVE_DEVICE_AGE_MS } from '../../../../../../src/components/views/settings/devices/filter';
import SettingsStore from '../../../../../../src/settings/SettingsStore';
mockPlatformPeg();
@ -1142,4 +1144,50 @@ describe('<SessionManagerTab />', () => {
expect(checkbox.getAttribute('aria-checked')).toEqual("false");
});
describe('QR code login', () => {
const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue');
beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(false);
// enable server support for qr login
mockClient.getVersions.mockResolvedValue({
versions: [],
unstable_features: {
'org.matrix.msc3882': true,
'org.matrix.msc3886': true,
},
});
});
it('does not render qr code login section when disabled', () => {
settingsValueSpy.mockReturnValue(false);
const { queryByText } = render(getComponent());
expect(settingsValueSpy).toHaveBeenCalledWith('feature_qr_signin_reciprocate_show');
expect(queryByText('Sign in with QR code')).toBeFalsy();
});
it('renders qr code login section when enabled', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText } = render(getComponent());
// wait for versions call to settle
await flushPromises();
expect(getByText('Sign in with QR code')).toBeTruthy();
});
it('enters qr code login section when show QR code button clicked', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText, getByTestId } = render(getComponent());
// wait for versions call to settle
await flushPromises();
fireEvent.click(getByText('Show QR code'));
expect(getByTestId("login-with-qr")).toBeTruthy();
});
});
});

View file

@ -38,6 +38,8 @@ import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingSt
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
import SettingsStore from "../../src/settings/SettingsStore";
import Modal, { IHandle } from "../../src/Modal";
import PlatformPeg from "../../src/PlatformPeg";
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
[MediaDeviceKindEnum.AudioInput]: [
@ -807,6 +809,69 @@ describe("ElementCall", () => {
call.off(CallEvent.Layout, onLayout);
});
describe("screensharing", () => {
it("passes source id if we can get it", async () => {
const sourceId = "source_id";
jest.spyOn(Modal, "createDialog").mockReturnValue(
{ finished: new Promise((r) => r([sourceId])) } as IHandle<any[]>,
);
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true);
await call.connect();
messaging.emit(
`action:${ElementWidgetActions.Screenshare}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
waitFor(() => {
expect(messaging!.transport.reply).toHaveBeenCalledWith(
expect.objectContaining({}),
expect.objectContaining({ desktopCapturerSourceId: sourceId }),
);
});
});
it("passes failed if we couldn't get a source id", async () => {
jest.spyOn(Modal, "createDialog").mockReturnValue(
{ finished: new Promise((r) => r([null])) } as IHandle<any[]>,
);
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true);
await call.connect();
messaging.emit(
`action:${ElementWidgetActions.Screenshare}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
waitFor(() => {
expect(messaging!.transport.reply).toHaveBeenCalledWith(
expect.objectContaining({}),
expect.objectContaining({ failed: true }),
);
});
});
it("passes an empty object if we don't support desktop capturer", async () => {
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(false);
await call.connect();
messaging.emit(
`action:${ElementWidgetActions.Screenshare}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
waitFor(() => {
expect(messaging!.transport.reply).toHaveBeenCalledWith(
expect.objectContaining({}),
expect.objectContaining({}),
);
});
});
});
it("ends the call immediately if we're the last participant to leave", async () => {
await call.connect();
const onDestroy = jest.fn();

View file

@ -12,6 +12,7 @@ exports[`Module Components should override the factory for a ModuleSpinner 1`] =
<div
aria-label="Loading..."
className="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style={
Object {

View file

@ -27,7 +27,7 @@ import { MatrixDispatcher } from '../../src/dispatcher/dispatcher';
import { UPDATE_EVENT } from '../../src/stores/AsyncStore';
import { ActiveRoomChangedPayload } from '../../src/dispatcher/payloads/ActiveRoomChangedPayload';
import { SpaceStoreClass } from '../../src/stores/spaces/SpaceStore';
import { TestStores } from '../TestStores';
import { TestSdkContext } from '../TestSdkContext';
// mock out the injected classes
jest.mock('../../src/PosthogAnalytics');
@ -77,7 +77,7 @@ describe('RoomViewStore', function() {
// Make the RVS to test
dis = new MatrixDispatcher();
slidingSyncManager = new MockSlidingSyncManager();
const stores = new TestStores();
const stores = new TestSdkContext();
stores._SlidingSyncManager = slidingSyncManager;
stores._PosthogAnalytics = new MockPosthogAnalytics();
stores._SpaceStore = new MockSpaceStore();

View file

@ -17,13 +17,14 @@ limitations under the License.
import { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import TypingStore from "../../src/stores/TypingStore";
import { LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
import SettingsStore from "../../src/settings/SettingsStore";
import { TestSdkContext } from "../TestSdkContext";
jest.mock("../../src/settings/SettingsStore", () => ({
getValue: jest.fn(),
monitorSetting: jest.fn(),
}));
describe("TypingStore", () => {
@ -37,11 +38,12 @@ describe("TypingStore", () => {
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
beforeEach(() => {
typingStore = new TypingStore();
mockClient = {
sendTyping: jest.fn(),
} as unknown as MatrixClient;
MatrixClientPeg.get = () => mockClient;
const context = new TestSdkContext();
context.client = mockClient;
typingStore = new TypingStore(context);
mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
return settings[setting];
});

View file

@ -0,0 +1,107 @@
/*
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 { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Widget, WidgetKind } from "matrix-widget-api";
import { OIDCState, WidgetPermissionStore } from "../../../src/stores/widgets/WidgetPermissionStore";
import SettingsStore from "../../../src/settings/SettingsStore";
import { TestSdkContext } from "../../TestSdkContext";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { stubClient } from "../../test-utils";
jest.mock("../../../src/settings/SettingsStore");
describe("WidgetPermissionStore", () => {
let widgetPermissionStore: WidgetPermissionStore;
let mockClient: MatrixClient;
const userId = "@alice:localhost";
const roomId = "!room:localhost";
const w = new Widget({
id: "wid",
creatorUserId: userId,
type: "m.custom",
url: "https://invalid.address.here",
});
let settings = {}; // key value store
beforeEach(() => {
settings = {}; // clear settings
mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
return settings[setting];
});
mocked(SettingsStore.setValue).mockImplementation((settingName: string,
roomId: string | null,
level: SettingLevel,
value: any,
): Promise<void> => {
// the store doesn't use any specific level or room ID (room IDs are packed into keys in `value`)
settings[settingName] = value;
return Promise.resolve();
});
mockClient = stubClient();
const context = new TestSdkContext();
context.client = mockClient;
widgetPermissionStore = new WidgetPermissionStore(context);
});
it("should persist OIDCState.Allowed for a widget", () => {
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Allowed);
// check it remembered the value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, null),
).toEqual(OIDCState.Allowed);
});
it("should persist OIDCState.Denied for a widget", () => {
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Denied);
// check it remembered the value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, null),
).toEqual(OIDCState.Denied);
});
it("should update OIDCState for a widget", () => {
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Allowed);
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Denied);
// check it remembered the latest value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, null),
).toEqual(OIDCState.Denied);
});
it("should scope the location for a widget when setting OIDC state", () => {
// allow this widget for this room
widgetPermissionStore.setOIDCState(w, WidgetKind.Room, roomId, OIDCState.Allowed);
// check it remembered the value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Room, roomId),
).toEqual(OIDCState.Allowed);
// check this is not the case for the entire account
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, roomId),
).toEqual(OIDCState.Unknown);
});
it("is created once in SdkContextClass", () => {
const context = new SdkContextClass();
const store = context.widgetPermissionStore;
expect(store).toBeDefined();
const store2 = context.widgetPermissionStore;
expect(store2).toStrictEqual(store);
});
});

View file

@ -22,6 +22,9 @@ import {
Room,
} from "matrix-js-sdk/src/matrix";
import { IRoomState } from "../../src/components/structures/RoomView";
import { TimelineRenderingType } from "../../src/contexts/RoomContext";
import { Layout } from "../../src/settings/enums/Layout";
import { mkEvent } from "./test-utils";
export const makeMembershipEvent = (
@ -50,3 +53,45 @@ export const makeRoomWithStateEvents = (
mockClient.getRoom.mockReturnValue(room1);
return room1;
};
export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoomState {
return {
room,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
...override,
};
}

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
import { Thread } from "matrix-js-sdk/src/models/thread";
import { mkMessage, MessageEventProps } from "./test-utils";
@ -78,7 +79,7 @@ export const makeThreadEvents = ({
rootEvent.setUnsigned({
"m.relations": {
"m.thread": {
[RelationType.Thread]: {
latest_event: events[events.length - 1],
count: length,
current_user_participated: [...participantUserIds, authorId].includes(currentUserId),
@ -88,3 +89,36 @@ export const makeThreadEvents = ({
return { rootEvent, events };
};
type MakeThreadProps = {
room: Room;
client: MatrixClient;
authorId: string;
participantUserIds: string[];
length?: number;
ts?: number;
};
export const mkThread = ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent } => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
participantUserIds,
length,
ts,
currentUserId: client.getUserId(),
});
const thread = room.createThread(rootEvent.getId(), rootEvent, events, true);
// So that we do not have to mock the thread loading
thread.initialEventsFetched = true;
return { thread, rootEvent };
};

View file

@ -17,11 +17,10 @@ limitations under the License.
import React from "react";
import { act, render, screen } from "@testing-library/react";
import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastBody,
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastRecordingBody,
VoiceBroadcastRecordingsStore,
@ -30,8 +29,8 @@ import {
VoiceBroadcastPlayback,
VoiceBroadcastPlaybacksStore,
} from "../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../test-utils";
import { RelationsHelper } from "../../../src/events/RelationsHelper";
import { stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
VoiceBroadcastRecordingBody: jest.fn(),
@ -41,27 +40,15 @@ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlayb
VoiceBroadcastPlaybackBody: jest.fn(),
}));
jest.mock("../../../src/events/RelationsHelper");
describe("VoiceBroadcastBody", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let infoEvent: MatrixEvent;
let stoppedEvent: MatrixEvent;
let testRecording: VoiceBroadcastRecording;
let testPlayback: VoiceBroadcastPlayback;
const mkVoiceBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: client.getUserId(),
room: roomId,
content: {
state,
},
});
};
const renderVoiceBroadcast = () => {
render(<VoiceBroadcastBody
mxEvent={infoEvent}
@ -75,7 +62,25 @@ describe("VoiceBroadcastBody", () => {
beforeEach(() => {
client = stubClient();
infoEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started);
room = new Room(roomId, client, client.getUserId());
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
if (getRoomId === roomId) return room;
});
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId(),
client.getDeviceId(),
infoEvent,
);
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
testRecording = new VoiceBroadcastRecording(infoEvent, client);
testPlayback = new VoiceBroadcastPlayback(infoEvent, client);
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }) => {
@ -107,7 +112,18 @@ describe("VoiceBroadcastBody", () => {
);
});
describe("when displaying a voice broadcast recording", () => {
describe("when there is a stopped voice broadcast", () => {
beforeEach(() => {
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
renderVoiceBroadcast();
});
it("should render a voice broadcast playback body", () => {
screen.getByTestId("voice-broadcast-playback-body");
});
});
describe("when there is a started voice broadcast from the current user", () => {
beforeEach(() => {
renderVoiceBroadcast();
});
@ -118,13 +134,8 @@ describe("VoiceBroadcastBody", () => {
describe("and the recordings ends", () => {
beforeEach(() => {
const stoppedEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped);
// get the RelationsHelper instanced used in VoiceBroadcastBody
const relationsHelper = mocked(RelationsHelper).mock.instances[5];
act(() => {
// invoke the callback of the VoiceBroadcastBody hook to simulate an ended broadcast
// @ts-ignore
mocked(relationsHelper.on).mock.calls[0][1](stoppedEvent);
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
});
});

View file

@ -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 React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PlaybackControlButton, VoiceBroadcastPlaybackState } from "../../../../src/voice-broadcast";
describe("PlaybackControlButton", () => {
let onClick: () => void;
beforeEach(() => {
onClick = jest.fn();
});
it.each([
[VoiceBroadcastPlaybackState.Playing],
[VoiceBroadcastPlaybackState.Paused],
[VoiceBroadcastPlaybackState.Stopped],
])("should render state »%s« as expected", (state: VoiceBroadcastPlaybackState) => {
const result = render(<PlaybackControlButton state={state} onClick={onClick} />);
expect(result.container).toMatchSnapshot();
});
it("should call onClick on click", async () => {
render(<PlaybackControlButton state={VoiceBroadcastPlaybackState.Playing} onClick={onClick} />);
const button = screen.getByLabelText("pause voice broadcast");
await userEvent.click(button);
expect(onClick).toHaveBeenCalled();
});
});

View file

@ -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 React from "react";
import { render, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { StopButton } from "../../../../src/voice-broadcast";
describe("StopButton", () => {
let result: RenderResult;
let onClick: () => {};
beforeEach(() => {
onClick = jest.fn();
result = render(<StopButton onClick={onClick} />);
});
it("should render as expected", () => {
expect(result.container).toMatchSnapshot();
});
describe("when clicking it", () => {
beforeEach(async () => {
await userEvent.click(result.getByLabelText("stop voice broadcast"));
});
it("should invoke the callback", () => {
expect(onClick).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,55 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { render, RenderResult, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { VoiceBroadcastControl } from "../../../../src/voice-broadcast";
import { Icon as StopIcon } from "../../../../res/img/element-icons/Stop.svg";
describe("VoiceBroadcastControl", () => {
let result: RenderResult;
let onClick: () => void;
beforeEach(() => {
onClick = jest.fn();
});
describe("when rendering it", () => {
beforeEach(() => {
result = render(<VoiceBroadcastControl
onClick={onClick}
label="test label"
icon={StopIcon}
/>);
});
it("should render as expected", () => {
expect(result.container).toMatchSnapshot();
});
describe("when clicking it", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("test label"));
});
it("should call onClick", () => {
expect(onClick).toHaveBeenCalled();
});
});
});
});

View file

@ -5,11 +5,8 @@ exports[`LiveBadge should render the expected HTML 1`] = `
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PlaybackControlButton should render state »0« as expected 1`] = `
<div>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;
exports[`PlaybackControlButton should render state »1« as expected 1`] = `
<div>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;
exports[`PlaybackControlButton should render state »2« as expected 1`] = `
<div>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;

View file

@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StopButton should render as expected 1`] = `
<div>
<div
aria-label="stop voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;

View file

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastControl when rendering it should render as expected 1`] = `
<div>
<div
aria-label="test label"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
`;

View file

@ -22,11 +22,8 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
test user
@ -35,11 +32,8 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
@ -47,11 +41,8 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -81,11 +72,8 @@ exports[`VoiceBroadcastHeader when rendering a non-live broadcast header should
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
test user

View file

@ -64,9 +64,6 @@ describe("VoiceBroadcastPlaybackBody", () => {
describe("when rendering a buffering voice broadcast", () => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering);
});
beforeEach(() => {
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
@ -75,18 +72,15 @@ describe("VoiceBroadcastPlaybackBody", () => {
});
});
describe("when rendering a broadcast", () => {
describe(`when rendering a ${VoiceBroadcastPlaybackState.Stopped} broadcast`, () => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped);
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and clicking the play button", () => {
beforeEach(async () => {
await userEvent.click(renderResult.getByLabelText("resume voice broadcast"));
await userEvent.click(renderResult.getByLabelText("play voice broadcast"));
});
it("should toggle the recording", () => {
@ -94,4 +88,18 @@ describe("VoiceBroadcastPlaybackBody", () => {
});
});
});
describe.each([
VoiceBroadcastPlaybackState.Paused,
VoiceBroadcastPlaybackState.Playing,
])("when rendering a %s broadcast", (playbackState: VoiceBroadcastPlaybackState) => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(playbackState);
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
});

View file

@ -20,6 +20,7 @@ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingBody,
} from "../../../../src/voice-broadcast";
@ -49,7 +50,7 @@ describe("VoiceBroadcastRecordingBody", () => {
room: roomId,
user: userId,
});
recording = new VoiceBroadcastRecording(infoEvent, client);
recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Running);
});
describe("when rendering a live broadcast", () => {

View file

@ -22,12 +22,12 @@ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import {
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
} from "../../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../../test-utils";
import { stubClient } from "../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
// mock RoomAvatar, because it is doing too much fancy stuff
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
@ -37,39 +37,52 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
}),
}));
jest.mock("../../../../src/audio/VoiceRecording");
describe("VoiceBroadcastRecordingPip", () => {
const userId = "@user:example.com";
const roomId = "!room:example.com";
let client: MatrixClient;
let infoEvent: MatrixEvent;
let recording: VoiceBroadcastRecording;
let renderResult: RenderResult;
const renderPip = (state: VoiceBroadcastInfoState) => {
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
state,
client.getUserId(),
client.getDeviceId(),
);
recording = new VoiceBroadcastRecording(infoEvent, client, state);
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
};
beforeAll(() => {
client = stubClient();
infoEvent = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
content: {},
room: roomId,
user: userId,
});
recording = new VoiceBroadcastRecording(infoEvent, client);
});
describe("when rendering", () => {
let renderResult: RenderResult;
describe("when rendering a started recording", () => {
beforeEach(() => {
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
renderPip(VoiceBroadcastInfoState.Started);
});
it("should create the expected result", () => {
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and clicking the pause button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("pause voice broadcast"));
});
it("should pause the recording", () => {
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Paused);
});
});
describe("and clicking the stop button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("stop voice broadcast"));
await userEvent.click(screen.getByLabelText("Stop Recording"));
// modal rendering has some weird sleeps
await sleep(100);
});
@ -89,4 +102,24 @@ describe("VoiceBroadcastRecordingPip", () => {
});
});
});
describe("when rendering a paused recording", () => {
beforeEach(() => {
renderPip(VoiceBroadcastInfoState.Paused);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and clicking the resume button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("resume voice broadcast"));
});
it("should resume the recording", () => {
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Running);
});
});
});
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as expected 1`] = `
exports[`VoiceBroadcastPlaybackBody when rendering a 0 broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastPlaybackBody"
@ -25,11 +25,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@ -38,11 +35,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
@ -50,11 +44,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -64,15 +55,80 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a 1 broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastPlaybackBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<div
class="mx_VoiceBroadcastPlaybackBody_controls"
>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
@ -105,11 +161,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@ -118,11 +171,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
@ -130,11 +180,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -148,6 +195,7 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>

View file

@ -25,11 +25,8 @@ exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should rend
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@ -39,11 +36,8 @@ exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should rend
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastRecordingPip when rendering should create the expected result 1`] = `
exports[`VoiceBroadcastRecordingPip when rendering a paused recording should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastRecordingPip"
@ -25,25 +25,19 @@ exports[`VoiceBroadcastRecordingPip when rendering should create the expected re
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@userId:matrix.org
</span>
</div>
</div>
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -55,16 +49,96 @@ exports[`VoiceBroadcastRecordingPip when rendering should create the expected re
class="mx_VoiceBroadcastRecordingPip_controls"
>
<div
aria-label="stop voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-recording"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="Stop Recording"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastRecordingPip when rendering a started recording should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastRecordingPip"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@userId:matrix.org
</span>
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<hr
class="mx_VoiceBroadcastRecordingPip_divider"
/>
<div
class="mx_VoiceBroadcastRecordingPip_controls"
>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="Stop Recording"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>

View file

@ -20,6 +20,7 @@ import {
EventType,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
MsgType,
RelationType,
Room,
@ -81,6 +82,7 @@ describe("VoiceBroadcastRecording", () => {
const setUpVoiceBroadcastRecording = () => {
voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged);
jest.spyOn(voiceBroadcastRecording, "destroy");
jest.spyOn(voiceBroadcastRecording, "removeAllListeners");
};
@ -90,6 +92,25 @@ describe("VoiceBroadcastRecording", () => {
});
};
const itShouldSendAnInfoEvent = (state: VoiceBroadcastInfoState) => {
it(`should send a ${state} info event`, () => {
expect(client.sendStateEvent).toHaveBeenCalledWith(
roomId,
VoiceBroadcastInfoEventType,
{
device_id: client.getDeviceId(),
state,
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: infoEvent.getId(),
},
},
client.getUserId(),
);
});
};
beforeEach(() => {
client = stubClient();
room = mkStubRoom(roomId, "Test Room", client);
@ -214,6 +235,18 @@ describe("VoiceBroadcastRecording", () => {
expect(voiceBroadcastRecorder.start).toHaveBeenCalled();
});
describe("and the info event is redacted", () => {
beforeEach(() => {
infoEvent.emit(MatrixEventEvent.BeforeRedaction, null, null);
});
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
it("should destroy the recording", () => {
expect(voiceBroadcastRecording.destroy).toHaveBeenCalled();
});
});
describe("and receiving a call action", () => {
beforeEach(() => {
dis.dispatch({
@ -341,6 +374,26 @@ describe("VoiceBroadcastRecording", () => {
});
});
describe.each([
["pause", async () => voiceBroadcastRecording.pause()],
["toggle", async () => voiceBroadcastRecording.toggle()],
])("and calling %s", (_case: string, action: Function) => {
beforeEach(async () => {
await action();
});
itShouldBeInState(VoiceBroadcastInfoState.Paused);
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused);
it("should stop the recorder", () => {
expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
});
it("should emit a paused state changed event", () => {
expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Paused);
});
});
describe("and calling destroy", () => {
beforeEach(() => {
voiceBroadcastRecording.destroy();
@ -356,6 +409,32 @@ describe("VoiceBroadcastRecording", () => {
});
});
});
describe("and it is in paused state", () => {
beforeEach(async () => {
await voiceBroadcastRecording.pause();
});
describe.each([
["resume", async () => voiceBroadcastRecording.resume()],
["toggle", async () => voiceBroadcastRecording.toggle()],
])("and calling %s", (_case: string, action: Function) => {
beforeEach(async () => {
await action();
});
itShouldBeInState(VoiceBroadcastInfoState.Running);
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Running);
it("should start the recorder", () => {
expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled();
});
it("should emit a running state changed event", () => {
expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Running);
});
});
});
});
describe("when created for a Voice Broadcast Info with a Stopped relation", () => {
@ -363,7 +442,7 @@ describe("VoiceBroadcastRecording", () => {
infoEvent = mkVoiceBroadcastInfoEvent({
device_id: client.getDeviceId(),
state: VoiceBroadcastInfoState.Started,
chunk_length: 300,
chunk_length: 120,
});
const relationsContainer = {

Some files were not shown because too many files have changed in this diff Show more