Merge branch 'develop' into staging
This commit is contained in:
commit
7919c69bf7
803 changed files with 9375 additions and 6928 deletions
|
@ -100,8 +100,12 @@ module.exports = {
|
|||
files: ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}", "cypress/**/*.ts"],
|
||||
extends: ["plugin:matrix-org/typescript", "plugin:matrix-org/react"],
|
||||
rules: {
|
||||
// temporary disabled
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/explicit-function-return-type": [
|
||||
"error",
|
||||
{
|
||||
allowExpressions: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Things we do that break the ideal style
|
||||
"prefer-promise-reject-errors": "off",
|
||||
|
|
33
.github/workflows/cypress.yaml
vendored
33
.github/workflows/cypress.yaml
vendored
|
@ -84,14 +84,16 @@ jobs:
|
|||
actions: read
|
||||
issues: read
|
||||
pull-requests: read
|
||||
environment:
|
||||
Cypress
|
||||
#strategy:
|
||||
# fail-fast: false
|
||||
# matrix:
|
||||
# # Run 4 instances in Parallel
|
||||
# runner: [1, 2, 3, 4]
|
||||
environment: Cypress
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Run 4 instances in Parallel
|
||||
runner: [1, 2, 3, 4]
|
||||
steps:
|
||||
- uses: browser-actions/setup-chrome@latest
|
||||
- run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV
|
||||
|
||||
- uses: tecolicom/actions-use-apt-tools@v1
|
||||
with:
|
||||
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
||||
|
@ -121,14 +123,12 @@ jobs:
|
|||
with:
|
||||
# The built-in Electron runner seems to grind to a halt trying
|
||||
# to run the tests, so use chrome.
|
||||
browser: chrome
|
||||
browser: "${{ env.BROWSER_PATH }}"
|
||||
start: npx serve -p 8080 webapp
|
||||
wait-on: "http://localhost:8080"
|
||||
record:
|
||||
true
|
||||
#parallel: true
|
||||
#command-prefix: 'yarn percy exec --parallel --'
|
||||
command-prefix: "yarn percy exec --"
|
||||
record: true
|
||||
parallel: true
|
||||
command-prefix: "yarn percy exec --parallel --"
|
||||
config: '{"reporter":"cypress-multi-reporters", "reporterOptions": { "configFile": "cypress-ci-reporter-config.json" } }'
|
||||
ci-build-id: ${{ needs.prepare.outputs.uuid }}
|
||||
env:
|
||||
|
@ -151,6 +151,8 @@ jobs:
|
|||
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
|
||||
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
|
||||
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
|
||||
CYPRESS_PULL_REQUEST_ID: ${{ needs.prepare.outputs.pr_id }}
|
||||
CYPRESS_PULL_REQUEST_URL: https://github.com/${{ github.repository }}/pull/${{ needs.prepare.outputs.pr_id }}
|
||||
|
||||
# pass the Percy token as an environment variable
|
||||
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
|
||||
|
@ -159,9 +161,8 @@ jobs:
|
|||
# tell Percy more details about the context of this run
|
||||
PERCY_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
PERCY_COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PERCY_PULL_REQUEST:
|
||||
${{ needs.prepare.outputs.pr_id }}
|
||||
#PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PULL_REQUEST: ${{ needs.prepare.outputs.pr_id }}
|
||||
PERCY_PARALLEL_TOTAL: ${{ strategy.job-total }}
|
||||
PERCY_PARALLEL_NONCE: ${{ needs.prepare.outputs.uuid }}
|
||||
|
||||
- name: Upload Artifact
|
||||
|
|
2
.github/workflows/i18n_check.yml
vendored
2
.github/workflows/i18n_check.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
- name: "Get modified files"
|
||||
id: changed_files
|
||||
if: github.event_name == 'pull_request' && github.event.pull_request.user.login != 'RiotTranslateBot'
|
||||
uses: tj-actions/changed-files@v34
|
||||
uses: tj-actions/changed-files@v35
|
||||
with:
|
||||
files: |
|
||||
src/i18n/strings/*
|
||||
|
|
|
@ -27,12 +27,11 @@ export default defineConfig({
|
|||
return require("./cypress/plugins/index.ts").default(on, config);
|
||||
},
|
||||
baseUrl: "http://localhost:8080",
|
||||
experimentalSessionAndOrigin: true,
|
||||
specPattern: "cypress/e2e/**/*.{js,jsx,ts,tsx}",
|
||||
},
|
||||
env: {
|
||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
||||
SLIDING_SYNC_PROXY_TAG: "v0.6.0",
|
||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
|
||||
SLIDING_SYNC_PROXY_TAG: "v0.99.0-rc1",
|
||||
HOMESERVER: "synapse",
|
||||
},
|
||||
retries: {
|
||||
|
|
|
@ -120,13 +120,27 @@ describe("Composer", () => {
|
|||
|
||||
// Type another
|
||||
cy.get("div[contenteditable=true]").type("my message 1");
|
||||
// Press enter. Would be nice to just use {enter} but we can't because Cypress
|
||||
// does not trigger an insertParagraph when you do that.
|
||||
cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" });
|
||||
// Send message
|
||||
cy.get("div[contenteditable=true]").type("{enter}");
|
||||
// It was sent
|
||||
cy.contains(".mx_EventTile_body", "my message 1");
|
||||
});
|
||||
|
||||
it("sends only one message when you press Enter multiple times", () => {
|
||||
// Type a message
|
||||
cy.get("div[contenteditable=true]").type("my message 0");
|
||||
// It has not been sent yet
|
||||
cy.contains(".mx_EventTile_body", "my message 0").should("not.exist");
|
||||
|
||||
// Click send
|
||||
cy.get("div[contenteditable=true]").type("{enter}");
|
||||
cy.get("div[contenteditable=true]").type("{enter}");
|
||||
cy.get("div[contenteditable=true]").type("{enter}");
|
||||
// It has been sent
|
||||
cy.contains(".mx_EventTile_body", "my message 0");
|
||||
cy.get(".mx_EventTile_body").should("have.length", 1);
|
||||
});
|
||||
|
||||
it("can write formatted text", () => {
|
||||
cy.get("div[contenteditable=true]").type("my {ctrl+b}bold{ctrl+b} message");
|
||||
cy.get('div[aria-label="Send message"]').click();
|
||||
|
@ -141,7 +155,7 @@ describe("Composer", () => {
|
|||
it("only sends when you press Ctrl+Enter", () => {
|
||||
// Type a message and press Enter
|
||||
cy.get("div[contenteditable=true]").type("my message 3");
|
||||
cy.get("div[contenteditable=true]").trigger("input", { inputType: "insertParagraph" });
|
||||
cy.get("div[contenteditable=true]").type("{enter}");
|
||||
// It has not been sent yet
|
||||
cy.contains(".mx_EventTile_body", "my message 3").should("not.exist");
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
|||
import type { CypressBot } from "../../support/bot";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { UserCredentials } from "../../support/login";
|
||||
|
||||
type EmojiMapping = [emoji: string, name: string];
|
||||
interface CryptoTestContext extends Mocha.Context {
|
||||
|
@ -154,11 +155,15 @@ const verify = function (this: CryptoTestContext) {
|
|||
};
|
||||
|
||||
describe("Cryptography", function () {
|
||||
let aliceCredentials: UserCredentials;
|
||||
|
||||
beforeEach(function () {
|
||||
cy.startHomeserver("default")
|
||||
.as("homeserver")
|
||||
.then((homeserver: HomeserverInstance) => {
|
||||
cy.initTestUser(homeserver, "Alice", undefined, "alice_");
|
||||
cy.initTestUser(homeserver, "Alice", undefined, "alice_").then((credentials) => {
|
||||
aliceCredentials = credentials;
|
||||
});
|
||||
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
||||
});
|
||||
});
|
||||
|
@ -183,7 +188,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("creating a DM should work, being e2e-encrypted / user verification", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
startDMWithBob.call(this);
|
||||
// send first message
|
||||
cy.get(".mx_BasicMessageComposer_input").click().should("have.focus").type("Hey!{enter}");
|
||||
|
@ -194,7 +199,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("should allow verification when there is no existing DM", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
autoJoin(this.bob);
|
||||
|
||||
// we need to have a room with the other user present, so we can open the verification panel
|
||||
|
@ -212,7 +217,7 @@ describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
it("should show the correct shield on edited e2e events", function (this: CryptoTestContext) {
|
||||
cy.bootstrapCrossSigning();
|
||||
cy.bootstrapCrossSigning(aliceCredentials);
|
||||
|
||||
// bob has a second, not cross-signed, device
|
||||
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
||||
|
|
|
@ -105,15 +105,9 @@ describe("Decryption Failure Bar", () => {
|
|||
"and there are other verified devices or backups",
|
||||
() => {
|
||||
let otherDevice: MatrixClient | undefined;
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, {})
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true })
|
||||
.then(async (cli) => {
|
||||
otherDevice = cli;
|
||||
await otherDevice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
await makeRequest({});
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
|
@ -169,15 +163,11 @@ describe("Decryption Failure Bar", () => {
|
|||
"should prompt the user to reset keys, if this device isn't verified " +
|
||||
"and there are no other verified devices or backups",
|
||||
() => {
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, {}).then(async (cli) => {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
await makeRequest({});
|
||||
},
|
||||
setupNewCrossSigning: true,
|
||||
});
|
||||
await cli.logout(true);
|
||||
});
|
||||
cy.loginBot(homeserver, testUser.username, testUser.password, { bootstrapCrossSigning: true }).then(
|
||||
async (cli) => {
|
||||
await cli.logout(true);
|
||||
},
|
||||
);
|
||||
|
||||
cy.botSendMessage(bot, roomId, "test");
|
||||
cy.wait(5000);
|
||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
|||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import type { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import type { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
||||
import type { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
||||
|
@ -29,6 +29,16 @@ const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
|
|||
});
|
||||
};
|
||||
|
||||
/** generate a message event which will take up some room on the page. */
|
||||
function mkPadding(n: number): IContent {
|
||||
return {
|
||||
msgtype: "m.text" as MsgType,
|
||||
body: `padding ${n}`,
|
||||
format: "org.matrix.custom.html",
|
||||
formatted_body: `<h3>Test event ${n}</h3>\n`.repeat(10),
|
||||
};
|
||||
}
|
||||
|
||||
describe("Editing", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
|
||||
|
@ -37,7 +47,6 @@ describe("Editing", () => {
|
|||
homeserver = data;
|
||||
cy.initTestUser(homeserver, "Edith").then(() => {
|
||||
cy.injectAxe();
|
||||
return cy.createRoom({ name: "Test room" }).as("roomId");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -47,6 +56,8 @@ describe("Editing", () => {
|
|||
});
|
||||
|
||||
it("should close the composer when clicking save after making a change and undoing it", () => {
|
||||
cy.createRoom({ name: "Test room" }).as("roomId");
|
||||
|
||||
cy.get<string>("@roomId").then((roomId) => {
|
||||
sendEvent(roomId);
|
||||
cy.visit("/#/room/" + roomId);
|
||||
|
@ -64,4 +75,77 @@ describe("Editing", () => {
|
|||
// Assert that the edit composer has gone away
|
||||
cy.get(".mx_EditMessageComposer").should("not.exist");
|
||||
});
|
||||
|
||||
it("should correctly display events which are edited, where we lack the edit event", () => {
|
||||
// This tests the behaviour when a message has been edited some time after it has been sent, and we
|
||||
// jump back in room history to view the event, but do not have the actual edit event.
|
||||
//
|
||||
// In that scenario, we rely on the server to replace the content (pre-MSC3925), or do it ourselves based on
|
||||
// the bundled edit event (post-MSC3925).
|
||||
//
|
||||
// To test it, we need to have a room with lots of events in, so we can jump around the timeline without
|
||||
// paginating in the event itself. Hence, we create a bot user which creates the room and populates it before
|
||||
// we join.
|
||||
|
||||
let testRoomId: string;
|
||||
let originalEventId: string;
|
||||
let editEventId: string;
|
||||
|
||||
// create a second user
|
||||
const bobChainable = cy.getBot(homeserver, { displayName: "Bob", userIdPrefix: "bob_" });
|
||||
|
||||
cy.all([cy.window({ log: false }), bobChainable]).then(async ([win, bob]) => {
|
||||
// "bob" now creates the room, and sends a load of events in it. Note that all of this happens via calls on
|
||||
// the js-sdk rather than Cypress commands, so uses regular async/await.
|
||||
|
||||
const room = await bob.createRoom({ name: "TestRoom", visibility: win.matrixcs.Visibility.Public });
|
||||
testRoomId = room.room_id;
|
||||
cy.log(`Bot user created room ${room.room_id}`);
|
||||
|
||||
originalEventId = (await bob.sendMessage(room.room_id, { body: "original", msgtype: "m.text" })).event_id;
|
||||
cy.log(`Bot user sent original event ${originalEventId}`);
|
||||
|
||||
// send a load of padding events. We make them large, so that they fill the whole screen
|
||||
// and the client doesn't end up paginating into the event we want.
|
||||
let i = 0;
|
||||
while (i < 10) {
|
||||
await bob.sendMessage(room.room_id, mkPadding(i++));
|
||||
}
|
||||
|
||||
// ... then the edit ...
|
||||
editEventId = (
|
||||
await bob.sendMessage(room.room_id, {
|
||||
"m.new_content": { body: "Edited body", msgtype: "m.text" },
|
||||
"m.relates_to": {
|
||||
rel_type: "m.replace",
|
||||
event_id: originalEventId,
|
||||
},
|
||||
"body": "* edited",
|
||||
"msgtype": "m.text",
|
||||
})
|
||||
).event_id;
|
||||
cy.log(`Bot user sent edit event ${editEventId}`);
|
||||
|
||||
// ... then a load more padding ...
|
||||
while (i < 20) {
|
||||
await bob.sendMessage(room.room_id, mkPadding(i++));
|
||||
}
|
||||
});
|
||||
|
||||
cy.getClient().then((cli) => {
|
||||
// now have the cypress user join the room, jump to the original event, and wait for the event to be
|
||||
// visible
|
||||
cy.joinRoom(testRoomId);
|
||||
cy.viewRoomByName("TestRoom");
|
||||
cy.visit(`#/room/${testRoomId}/${originalEventId}`);
|
||||
cy.get(`[data-event-id="${originalEventId}"]`).should((messageTile) => {
|
||||
// at this point, the edit event should still be unknown
|
||||
expect(cli.getRoom(testRoomId).getTimelineForEvent(editEventId)).to.be.null;
|
||||
|
||||
// nevertheless, the event should be updated
|
||||
expect(messageTile.find(".mx_EventTile_body").text()).to.eq("Edited body");
|
||||
expect(messageTile.find(".mx_EventTile_edited")).to.exist;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -82,9 +82,27 @@ function sendActionFromIntegrationManager(integrationManagerUrl: string, targetR
|
|||
});
|
||||
}
|
||||
|
||||
function clickUntilGone(selector: string, attempt = 0) {
|
||||
if (attempt === 11) {
|
||||
throw new Error("clickUntilGone attempt count exceeded");
|
||||
}
|
||||
|
||||
cy.get(selector)
|
||||
.last()
|
||||
.click()
|
||||
.then(($button) => {
|
||||
const exists = Cypress.$(selector).length > 0;
|
||||
if (exists) {
|
||||
clickUntilGone(selector, ++attempt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function expectKickedMessage(shouldExist: boolean) {
|
||||
// Expand any event summaries
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click({ multiple: true });
|
||||
// Expand any event summaries, we can't use a click multiple here because clicking one might de-render others
|
||||
// This is quite horrible but seems the most stable way of clicking 0-N buttons,
|
||||
// one at a time with a full re-evaluation after each click
|
||||
clickUntilGone(".mx_GenericEventListSummary_toggle[aria-expanded=false]");
|
||||
|
||||
// Check for the event message (or lack thereof)
|
||||
cy.contains(".mx_EventTile_line", `${USER_DISPLAY_NAME} removed ${BOT_DISPLAY_NAME}: ${KICK_REASON}`).should(
|
||||
|
|
|
@ -16,8 +16,6 @@ limitations under the License.
|
|||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { PollResponseEvent } from "matrix-events-sdk";
|
||||
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { MatrixClient } from "../../global";
|
||||
import Chainable = Cypress.Chainable;
|
||||
|
@ -70,8 +68,16 @@ describe("Polls", () => {
|
|||
cy.get('input[type="radio"]')
|
||||
.invoke("attr", "value")
|
||||
.then((optionId) => {
|
||||
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
|
||||
bot.sendEvent(roomId, pollVote.type, pollVote.content);
|
||||
// We can't use the js-sdk types for this stuff directly, so manually construct the event.
|
||||
bot.sendEvent(roomId, "org.matrix.msc3381.poll.response", {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: pollId,
|
||||
},
|
||||
"org.matrix.msc3381.poll.response": {
|
||||
answers: [optionId],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -21,8 +21,6 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
|||
import { Interception } from "cypress/types/net-stubbing";
|
||||
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { Layout } from "../../../src/settings/enums/Layout";
|
||||
import { ProxyInstance } from "../../plugins/sliding-sync";
|
||||
|
||||
describe("Sliding Sync", () => {
|
||||
|
@ -102,21 +100,6 @@ describe("Sliding Sync", () => {
|
|||
});
|
||||
};
|
||||
|
||||
// sanity check everything works
|
||||
it("should correctly render expected messages", () => {
|
||||
cy.get<string>("@roomId").then((roomId) => cy.visit("/#/room/" + roomId));
|
||||
cy.setSettingValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
|
||||
// Wait until configuration is finished
|
||||
cy.contains(
|
||||
".mx_RoomView_body .mx_GenericEventListSummary .mx_GenericEventListSummary_summary",
|
||||
"created and configured the room.",
|
||||
);
|
||||
|
||||
// Click "expand" link button
|
||||
cy.get(".mx_GenericEventListSummary_toggle[aria-expanded=false]").click();
|
||||
});
|
||||
|
||||
it("should render the Rooms list in reverse chronological order by default and allowing sorting A-Z", () => {
|
||||
// create rooms and check room names are correct
|
||||
cy.createRoom({ name: "Apple" }).then(() => cy.contains(".mx_RoomSublist", "Apple"));
|
||||
|
|
221
cypress/e2e/widgets/events.spec.ts
Normal file
221
cypress/e2e/widgets/events.spec.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
Copyright 2022 Mikhail Aheichyk
|
||||
Copyright 2022 Nordeck IT + Consulting GmbH.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
|
||||
|
||||
import type { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||
import { UserCredentials } from "../../support/login";
|
||||
|
||||
const DEMO_WIDGET_ID = "demo-widget-id";
|
||||
const DEMO_WIDGET_NAME = "Demo Widget";
|
||||
const DEMO_WIDGET_TYPE = "demo";
|
||||
const ROOM_NAME = "Demo";
|
||||
|
||||
const DEMO_WIDGET_HTML = `
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Demo Widget</title>
|
||||
<script>
|
||||
let sendEventCount = 0
|
||||
window.onmessage = ev => {
|
||||
if (ev.data.action === 'capabilities') {
|
||||
window.parent.postMessage(Object.assign({
|
||||
response: {
|
||||
capabilities: [
|
||||
"org.matrix.msc2762.timeline:*",
|
||||
"org.matrix.msc2762.receive.state_event:m.room.topic",
|
||||
"org.matrix.msc2762.send.event:net.widget_echo"
|
||||
]
|
||||
},
|
||||
}, ev.data), '*');
|
||||
} else if (ev.data.action === 'send_event' && !ev.data.response) {
|
||||
// wraps received event into 'net.widget_echo' and sends back
|
||||
sendEventCount += 1
|
||||
window.parent.postMessage({
|
||||
api: "fromWidget",
|
||||
widgetId: ev.data.widgetId,
|
||||
requestId: 'widget-' + sendEventCount,
|
||||
action: "send_event",
|
||||
data: {
|
||||
type: 'net.widget_echo',
|
||||
content: ev.data.data // sets matrix event to the content returned
|
||||
},
|
||||
}, '*')
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<button id="demo">Demo</button>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
function waitForRoom(win: Cypress.AUTWindow, roomId: string, predicate: (room: Room) => boolean): Promise<void> {
|
||||
const matrixClient = win.mxMatrixClientPeg.get();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const room = matrixClient.getRoom(roomId);
|
||||
|
||||
if (predicate(room)) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
function onEvent(ev: MatrixEvent) {
|
||||
if (ev.getRoomId() !== roomId) return;
|
||||
|
||||
if (predicate(room)) {
|
||||
matrixClient.removeListener(win.matrixcs.ClientEvent.Event, onEvent);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
|
||||
matrixClient.on(win.matrixcs.ClientEvent.Event, onEvent);
|
||||
});
|
||||
}
|
||||
|
||||
describe("Widget Events", () => {
|
||||
let homeserver: HomeserverInstance;
|
||||
let user: UserCredentials;
|
||||
let bot: MatrixClient;
|
||||
let demoWidgetUrl: string;
|
||||
|
||||
beforeEach(() => {
|
||||
cy.startHomeserver("default").then((data) => {
|
||||
homeserver = data;
|
||||
|
||||
cy.initTestUser(homeserver, "Mike").then((_user) => {
|
||||
user = _user;
|
||||
});
|
||||
cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: true }).then((_bot) => {
|
||||
bot = _bot;
|
||||
});
|
||||
});
|
||||
cy.serveHtmlFile(DEMO_WIDGET_HTML).then((url) => {
|
||||
demoWidgetUrl = url;
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cy.stopHomeserver(homeserver);
|
||||
cy.stopWebServers();
|
||||
});
|
||||
|
||||
it("should be updated if user is re-invited into the room with updated state event", () => {
|
||||
cy.createRoom({
|
||||
name: ROOM_NAME,
|
||||
invite: [bot.getUserId()],
|
||||
}).then((roomId) => {
|
||||
// setup widget via state event
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content: IWidget = {
|
||||
id: DEMO_WIDGET_ID,
|
||||
creatorUserId: "somebody",
|
||||
type: DEMO_WIDGET_TYPE,
|
||||
name: DEMO_WIDGET_NAME,
|
||||
url: demoWidgetUrl,
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, "im.vector.modular.widgets", content, DEMO_WIDGET_ID);
|
||||
})
|
||||
.as("widgetEventSent");
|
||||
|
||||
// set initial layout
|
||||
cy.getClient()
|
||||
.then(async (matrixClient) => {
|
||||
const content = {
|
||||
widgets: {
|
||||
[DEMO_WIDGET_ID]: {
|
||||
container: "top",
|
||||
index: 1,
|
||||
width: 100,
|
||||
height: 0,
|
||||
},
|
||||
},
|
||||
};
|
||||
await matrixClient.sendStateEvent(roomId, "io.element.widgets.layout", content, "");
|
||||
})
|
||||
.as("layoutEventSent");
|
||||
|
||||
// open the room
|
||||
cy.viewRoomByName(ROOM_NAME);
|
||||
|
||||
// approve capabilities
|
||||
cy.contains(".mx_WidgetCapabilitiesPromptDialog button", "Approve").click();
|
||||
|
||||
cy.all([cy.get<string>("@widgetEventSent"), cy.get<string>("@layoutEventSent")]).then(async () => {
|
||||
// bot creates a new room with 'm.room.topic'
|
||||
const { room_id: roomNew } = await bot.createRoom({
|
||||
name: "New room",
|
||||
initial_state: [
|
||||
{
|
||||
type: "m.room.topic",
|
||||
state_key: "",
|
||||
content: {
|
||||
topic: "topic initial",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await bot.invite(roomNew, user.userId);
|
||||
|
||||
// widget should receive 'm.room.topic' event after invite
|
||||
cy.window().then(async (win) => {
|
||||
await waitForRoom(win, roomId, (room) => {
|
||||
const events = room.getLiveTimeline().getEvents();
|
||||
return events.some(
|
||||
(e) =>
|
||||
e.getType() === "net.widget_echo" &&
|
||||
e.getContent().type === "m.room.topic" &&
|
||||
e.getContent().content.topic === "topic initial",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// update the topic
|
||||
await bot.sendStateEvent(
|
||||
roomNew,
|
||||
"m.room.topic",
|
||||
{
|
||||
topic: "topic updated",
|
||||
},
|
||||
"",
|
||||
);
|
||||
|
||||
await bot.invite(roomNew, user.userId, "something changed in the room");
|
||||
|
||||
// widget should receive updated 'm.room.topic' event after re-invite
|
||||
cy.window().then(async (win) => {
|
||||
await waitForRoom(win, roomId, (room) => {
|
||||
const events = room.getLiveTimeline().getEvents();
|
||||
return events.some(
|
||||
(e) =>
|
||||
e.getType() === "net.widget_echo" &&
|
||||
e.getContent().type === "m.room.topic" &&
|
||||
e.getContent().content.topic === "topic updated",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,7 +23,7 @@ import { getFreePort } from "../utils/port";
|
|||
import { HomeserverInstance } from "../utils/homeserver";
|
||||
|
||||
// A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync
|
||||
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
||||
// SLIDING_SYNC_PROXY_TAG env used as the docker tag to use for `ghcr.io/matrix-org/sliding-sync` image.
|
||||
|
||||
export interface ProxyInstance {
|
||||
containerId: string;
|
||||
|
@ -72,7 +72,7 @@ async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Pr
|
|||
const port = await getFreePort();
|
||||
console.log(new Date(), "starting proxy container...", dockerTag);
|
||||
const containerId = await dockerRun({
|
||||
image: "ghcr.io/matrix-org/sliding-sync-proxy:" + dockerTag,
|
||||
image: "ghcr.io/matrix-org/sliding-sync:" + dockerTag,
|
||||
containerName: "react-sdk-cypress-sliding-sync-proxy",
|
||||
params: [
|
||||
"--rm",
|
||||
|
|
|
@ -150,7 +150,14 @@ function setupBotClient(
|
|||
if (opts.bootstrapCrossSigning) {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
await func({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import type { MatrixClient } from "matrix-js-sdk/src/client";
|
|||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { IContent } from "matrix-js-sdk/src/models/event";
|
||||
import Chainable = Cypress.Chainable;
|
||||
import { UserCredentials } from "./login";
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
|
@ -119,7 +120,7 @@ declare global {
|
|||
/**
|
||||
* Boostraps cross-signing.
|
||||
*/
|
||||
bootstrapCrossSigning(): Chainable<void>;
|
||||
bootstrapCrossSigning(credendtials: UserCredentials): Chainable<void>;
|
||||
/**
|
||||
* Joins the given room by alias or ID
|
||||
* @param roomIdOrAlias the id or alias of the room to join
|
||||
|
@ -210,11 +211,18 @@ Cypress.Commands.add("setAvatarUrl", (url: string): Chainable<{}> => {
|
|||
});
|
||||
});
|
||||
|
||||
Cypress.Commands.add("bootstrapCrossSigning", () => {
|
||||
Cypress.Commands.add("bootstrapCrossSigning", (credentials: UserCredentials) => {
|
||||
cy.window({ log: false }).then((win) => {
|
||||
win.mxMatrixClientPeg.matrixClient.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (func) => {
|
||||
await func({});
|
||||
await func({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
|
|
@ -76,6 +76,7 @@ export interface Credentials {
|
|||
userId: string;
|
||||
deviceId: string;
|
||||
homeServer: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
function registerUser(
|
||||
|
@ -120,6 +121,7 @@ function registerUser(
|
|||
accessToken: response.body.access_token,
|
||||
userId: response.body.user_id,
|
||||
deviceId: response.body.device_id,
|
||||
password: password,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2022-2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,12 +16,6 @@ limitations under the License.
|
|||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
// @see https://github.com/cypress-io/cypress/issues/915#issuecomment-475862672
|
||||
// Modified due to changes to `cy.queue` https://github.com/cypress-io/cypress/pull/17448
|
||||
// Note: this DOES NOT run Promises in parallel like `Promise.all` due to the nature
|
||||
// of Cypress promise-like objects and command queue. This only makes it convenient to use the same
|
||||
// API but runs the commands sequentially.
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace Cypress {
|
||||
|
@ -31,51 +25,35 @@ declare global {
|
|||
all<T extends Cypress.Chainable[] | []>(
|
||||
commands: T,
|
||||
): Cypress.Chainable<{ [P in keyof T]: ChainableValue<T[P]> }>;
|
||||
queue: any;
|
||||
}
|
||||
|
||||
interface Chainable {
|
||||
chainerId: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const chainStart = Symbol("chainStart");
|
||||
|
||||
/**
|
||||
* @description Returns a single Chainable that resolves when all of the Chainables pass.
|
||||
* @param {Cypress.Chainable[]} commands - List of Cypress.Chainable to resolve.
|
||||
* @returns {Cypress.Chainable} Cypress when all Chainables are resolved.
|
||||
*/
|
||||
cy.all = function all(commands): Cypress.Chainable {
|
||||
const chain = cy.wrap(null, { log: false });
|
||||
const stopCommand = Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: chain.chainerId },
|
||||
const resultArray = [];
|
||||
|
||||
// as each command completes, store the result in the corresponding location of resultArray.
|
||||
for (let i = 0; i < commands.length; i++) {
|
||||
commands[i].then((val) => {
|
||||
resultArray[i] = val;
|
||||
});
|
||||
}
|
||||
|
||||
// add an entry to the log which, when clicked, will write the results to the console.
|
||||
Cypress.log({
|
||||
name: "all",
|
||||
consoleProps: () => ({ Results: resultArray }),
|
||||
});
|
||||
const startCommand = Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: commands[0].chainerId },
|
||||
});
|
||||
const p = chain.then(() => {
|
||||
return cy.wrap(
|
||||
// @see https://lodash.com/docs/4.17.15#lodash
|
||||
Cypress._(commands)
|
||||
.map((cmd) => {
|
||||
return cmd[chainStart]
|
||||
? cmd[chainStart].attributes
|
||||
: Cypress._.find(cy.queue.get(), {
|
||||
attributes: { chainerId: cmd.chainerId },
|
||||
}).attributes;
|
||||
})
|
||||
.concat(stopCommand.attributes)
|
||||
.slice(1)
|
||||
.map((cmd) => {
|
||||
return cmd.prev.get("subject");
|
||||
})
|
||||
.value(),
|
||||
);
|
||||
});
|
||||
p[chainStart] = startCommand;
|
||||
return p;
|
||||
|
||||
// return a chainable which wraps the resultArray. Although this doesn't have a direct dependency on the input
|
||||
// commands, cypress won't process it until the commands that precede it on the command queue (which must include
|
||||
// the input commands) have passed.
|
||||
return cy.wrap(resultArray, { log: false });
|
||||
};
|
||||
|
||||
// Needed to make this file a module
|
||||
|
|
11
package.json
11
package.json
|
@ -23,7 +23,7 @@
|
|||
"package.json",
|
||||
".stylelintrc.js"
|
||||
],
|
||||
"main": "./lib/index.ts",
|
||||
"main": "./src/index.ts",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_lib_main": "./lib/index.ts",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
|
@ -56,8 +56,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@matrix-org/analytics-events": "^0.3.0",
|
||||
"@matrix-org/matrix-wysiwyg": "^0.14.0",
|
||||
"@matrix-org/analytics-events": "^0.4.0",
|
||||
"@matrix-org/matrix-wysiwyg": "^0.20.0",
|
||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||
"@sentry/browser": "^7.0.0",
|
||||
"@sentry/tracing": "^7.0.0",
|
||||
|
@ -177,7 +177,7 @@
|
|||
"babel-jest": "^29.0.0",
|
||||
"blob-polyfill": "^7.0.0",
|
||||
"chokidar": "^3.5.1",
|
||||
"cypress": "^11.0.0",
|
||||
"cypress": "^12.0.0",
|
||||
"cypress-axe": "^1.0.0",
|
||||
"cypress-multi-reporters": "^1.6.1",
|
||||
"cypress-real-events": "^1.7.1",
|
||||
|
@ -259,6 +259,5 @@
|
|||
"outputDirectory": "coverage",
|
||||
"outputName": "jest-sonar-report.xml",
|
||||
"relativePaths": true
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -379,5 +379,6 @@
|
|||
@import "./voice-broadcast/atoms/_PlaybackControlButton.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss";
|
||||
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
|
||||
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";
|
||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
|||
|
||||
/* 1rem :: 10px */
|
||||
|
||||
$spacing-2: 2px;
|
||||
$spacing-4: 4px;
|
||||
$spacing-8: 8px;
|
||||
$spacing-12: 12px;
|
||||
|
|
|
@ -84,7 +84,7 @@ limitations under the License.
|
|||
align-items: center;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
&:focus-visible {
|
||||
background-color: $menu-selected-color;
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,10 @@ limitations under the License.
|
|||
mask-image: url("$(res)/img/element-icons/security.svg");
|
||||
}
|
||||
|
||||
.mx_UserSettingsDialog_sessionsIcon::before {
|
||||
mask-image: url("$(res)/img/element-icons/settings/devices.svg");
|
||||
}
|
||||
|
||||
.mx_UserSettingsDialog_helpIcon::before {
|
||||
mask-image: url("$(res)/img/element-icons/settings/help.svg");
|
||||
}
|
||||
|
|
|
@ -67,6 +67,8 @@ $button-gap: 24px;
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: $lightbox-fg-color;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
}
|
||||
|
||||
.mx_ImageView_info {
|
||||
|
@ -82,6 +84,9 @@ $button-gap: 24px;
|
|||
.mx_ImageView_title {
|
||||
color: $lightbox-fg-color;
|
||||
font-size: $font-12px;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mx_ImageView_toolbar {
|
||||
|
@ -89,6 +94,9 @@ $button-gap: 24px;
|
|||
pointer-events: initial;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
flex-basis: 0;
|
||||
justify-content: flex-end;
|
||||
gap: calc($button-gap - ($button-size - $icon-size));
|
||||
}
|
||||
|
||||
|
|
|
@ -548,7 +548,19 @@ $left-gutter: 64px;
|
|||
pre,
|
||||
code {
|
||||
font-family: $monospace-font-family !important;
|
||||
background-color: $codeblock-background-color;
|
||||
background-color: $system;
|
||||
}
|
||||
|
||||
code:not(pre *) {
|
||||
background-color: $inlinecode-background-color;
|
||||
border: 1px solid $inlinecode-border-color;
|
||||
border-radius: 4px;
|
||||
// The horizontal padding is added by gfm.css .markdown-body
|
||||
padding: $spacing-2 0;
|
||||
// Avoid inline code blocks to be sticked when on multiple lines
|
||||
line-height: $font-22px;
|
||||
// Avoid the border to be glued to the other words
|
||||
margin-right: $spacing-2;
|
||||
}
|
||||
|
||||
code {
|
||||
|
@ -566,6 +578,8 @@ $left-gutter: 64px;
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
border: 1px solid $quinary-content;
|
||||
|
||||
code {
|
||||
white-space: pre; /* we want code blocks to be scrollable and not wrap */
|
||||
|
||||
|
@ -619,6 +633,17 @@ $left-gutter: 64px;
|
|||
ul ol {
|
||||
list-style-type: revert;
|
||||
}
|
||||
|
||||
/* Make list type disc to match rich text editor */
|
||||
> ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
/* Remove top and bottom margin for better consecutive list display */
|
||||
> :is(ol, ul) {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -733,6 +758,8 @@ $left-gutter: 64px;
|
|||
|
||||
.mx_EventTile_collapsedCodeBlock {
|
||||
max-height: 30vh;
|
||||
padding-top: $spacing-12;
|
||||
padding-bottom: $spacing-12;
|
||||
}
|
||||
|
||||
/* Inserted adjacent to <pre> blocks, (See TextualBody) */
|
||||
|
@ -873,6 +900,7 @@ $left-gutter: 64px;
|
|||
|
||||
&::before {
|
||||
inset: 0;
|
||||
pointer-events: none; /* ensures the title for the sender name can be correctly displayed */
|
||||
}
|
||||
|
||||
/* Display notification dot */
|
||||
|
@ -916,8 +944,14 @@ $left-gutter: 64px;
|
|||
inset: $padding auto auto $padding;
|
||||
}
|
||||
|
||||
.mx_EventTile_details {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_DisambiguatedProfile {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.mx_DisambiguatedProfile_displayName,
|
||||
.mx_DisambiguatedProfile_mxid {
|
||||
|
@ -968,8 +1002,11 @@ $left-gutter: 64px;
|
|||
|
||||
.mx_MessageTimestamp {
|
||||
font-size: $font-12px;
|
||||
max-width: var(--MessageTimestamp-max-width);
|
||||
width: unset; /* Cancel the default width */
|
||||
overflow: hidden; /* ensure correct overflow behavior */
|
||||
text-overflow: ellipsis;
|
||||
position: initial;
|
||||
margin-left: auto; /* to ensure it's end-aligned even if it's the only element of its parent */
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
@ -1297,7 +1334,7 @@ $left-gutter: 64px;
|
|||
|
||||
.mx_EventTile_details {
|
||||
display: flex;
|
||||
width: -webkit-fill-available;
|
||||
width: stretch;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-8;
|
||||
|
|
|
@ -32,11 +32,12 @@ limitations under the License.
|
|||
grid-template:
|
||||
"sender" auto
|
||||
"message" auto
|
||||
/ auto;
|
||||
/ 100%;
|
||||
text-decoration: none;
|
||||
color: $secondary-content;
|
||||
transition: color ease 0.15s;
|
||||
gap: 2px;
|
||||
max-width: 100%; // avoid overflow with wide content
|
||||
|
||||
&:hover {
|
||||
color: $primary-content;
|
||||
|
|
|
@ -108,7 +108,7 @@ limitations under the License.
|
|||
display: flex;
|
||||
user-select: none;
|
||||
|
||||
&:not(.mx_RoomHeader_name--textonly):hover {
|
||||
&:hover {
|
||||
background-color: $quinary-content;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_WysiwygComposer_Editor_content {
|
||||
line-height: $font-22px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
outline: none;
|
||||
|
@ -35,6 +36,52 @@ limitations under the License.
|
|||
.caretNode {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
ul,
|
||||
ol {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding-inline-start: $spacing-28;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #777;
|
||||
border-left: 2px solid $blockquote-bar-color;
|
||||
border-radius: 2px;
|
||||
padding: 0 10px;
|
||||
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
// model output always includes a linebreak but we do not want the user
|
||||
// to see it when writing input in lists
|
||||
:is(ol, ul, pre, blockquote) + br:last-of-type {
|
||||
display: none;
|
||||
}
|
||||
|
||||
> pre {
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
padding: $spacing-8 $spacing-12;
|
||||
|
||||
background-color: $inlinecode-background-color;
|
||||
border: 1px solid $inlinecode-border-color;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: $monospace-font-family !important;
|
||||
background-color: $inlinecode-background-color;
|
||||
border: 1px solid $inlinecode-border-color;
|
||||
border-radius: 4px;
|
||||
padding: $spacing-2;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_WysiwygComposer_Editor_content_placeholder::before {
|
||||
|
|
|
@ -50,6 +50,12 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_FormattingButtons_disabled {
|
||||
.mx_FormattingButtons_Icon {
|
||||
color: $quinary-content;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_FormattingButtons_Icon {
|
||||
--size: 16px;
|
||||
height: var(--size);
|
||||
|
|
|
@ -17,4 +17,5 @@ limitations under the License.
|
|||
.mx_CallDuration {
|
||||
color: $secondary-content;
|
||||
font-size: $font-12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
@ -160,7 +160,7 @@ limitations under the License.
|
|||
content: "";
|
||||
display: inline-block;
|
||||
mask-image: url("$(res)/img/feather-customised/chevron-down.svg");
|
||||
mask-size: $size;
|
||||
mask-size: 20px;
|
||||
mask-position: center;
|
||||
background-color: $call-primary-content;
|
||||
height: 100%;
|
||||
|
@ -181,7 +181,7 @@ limitations under the License.
|
|||
.mx_CallView_deviceButton {
|
||||
&.mx_CallView_deviceButton_audio::before {
|
||||
mask-image: url("$(res)/img/element-icons/Mic-off.svg");
|
||||
mask-size: 14px;
|
||||
mask-size: 18px;
|
||||
}
|
||||
|
||||
&.mx_CallView_deviceButton_video::before {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_VoiceBroadcastRecordingConnectionError {
|
||||
align-items: center;
|
||||
color: $alert;
|
||||
display: flex;
|
||||
gap: $spacing-12;
|
||||
|
||||
svg path {
|
||||
fill: $alert;
|
||||
}
|
||||
}
|
3
res/img/element-icons/room/composer/bulleted_list.svg
Normal file
3
res/img/element-icons/room/composer/bulleted_list.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.66666 4C1.11332 4 0.666656 4.44667 0.666656 5C0.666656 5.55333 1.11332 6 1.66666 6C2.21999 6 2.66666 5.55333 2.66666 5C2.66666 4.44667 2.21999 4 1.66666 4ZM1.66666 0C1.11332 0 0.666656 0.446667 0.666656 1C0.666656 1.55333 1.11332 2 1.66666 2C2.21999 2 2.66666 1.55333 2.66666 1C2.66666 0.446667 2.21999 0 1.66666 0ZM1.66666 8C1.11332 8 0.666656 8.45333 0.666656 9C0.666656 9.54667 1.11999 10 1.66666 10C2.21332 10 2.66666 9.54667 2.66666 9C2.66666 8.45333 2.21999 8 1.66666 8ZM4.33332 9.66667H12.3333C12.7 9.66667 13 9.36667 13 9C13 8.63333 12.7 8.33333 12.3333 8.33333H4.33332C3.96666 8.33333 3.66666 8.63333 3.66666 9C3.66666 9.36667 3.96666 9.66667 4.33332 9.66667ZM4.33332 5.66667H12.3333C12.7 5.66667 13 5.36667 13 5C13 4.63333 12.7 4.33333 12.3333 4.33333H4.33332C3.96666 4.33333 3.66666 4.63333 3.66666 5C3.66666 5.36667 3.96666 5.66667 4.33332 5.66667ZM3.66666 1C3.66666 1.36667 3.96666 1.66667 4.33332 1.66667H12.3333C12.7 1.66667 13 1.36667 13 1C13 0.633333 12.7 0.333333 12.3333 0.333333H4.33332C3.96666 0.333333 3.66666 0.633333 3.66666 1Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
3
res/img/element-icons/room/composer/code_block.svg
Normal file
3
res/img/element-icons/room/composer/code_block.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.14288 6.99997L5.47622 5.66663C5.5905 5.55235 5.64765 5.41266 5.64765 5.24758C5.64765 5.0825 5.5905 4.94282 5.47622 4.82854C5.36193 4.71425 5.22225 4.65711 5.05717 4.65711C4.89209 4.65711 4.75241 4.71425 4.63812 4.82854L2.86669 6.59997C2.8032 6.66346 2.75876 6.72695 2.73336 6.79044C2.70796 6.85393 2.69526 6.92377 2.69526 6.99997C2.69526 7.07616 2.70796 7.146 2.73336 7.20949C2.75876 7.27298 2.8032 7.33647 2.86669 7.39996L4.65717 9.19044C4.77145 9.30473 4.91114 9.36187 5.07622 9.36187C5.2413 9.36187 5.38098 9.30473 5.49526 9.19044C5.60955 9.07616 5.66669 8.93647 5.66669 8.77139C5.66669 8.60631 5.60955 8.46663 5.49526 8.35235L4.14288 6.99997ZM9.85717 6.99997L8.50479 8.35235C8.3905 8.46663 8.33336 8.60631 8.33336 8.77139C8.33336 8.93647 8.3905 9.07616 8.50479 9.19044C8.61907 9.30473 8.75876 9.36187 8.92384 9.36187C9.08891 9.36187 9.2286 9.30473 9.34288 9.19044L11.1334 7.39996C11.1969 7.33647 11.2413 7.27298 11.2667 7.20949C11.2921 7.146 11.3048 7.07616 11.3048 6.99997C11.3048 6.92377 11.2921 6.85393 11.2667 6.79044C11.2413 6.72695 11.1969 6.66346 11.1334 6.59997L9.34288 4.80949C9.29209 4.746 9.2286 4.70155 9.15241 4.67616C9.07622 4.65076 9.00003 4.63806 8.92384 4.63806C8.84765 4.63806 8.77463 4.65076 8.70479 4.67616C8.63495 4.70155 8.56828 4.746 8.50479 4.80949C8.3905 4.92377 8.33336 5.06346 8.33336 5.22854C8.33336 5.39362 8.3905 5.5333 8.50479 5.64758L9.85717 6.99997ZM1.28574 13.8571C0.980979 13.8571 0.714312 13.7428 0.48574 13.5143C0.257169 13.2857 0.142883 13.019 0.142883 12.7143V1.28568C0.142883 0.980918 0.257169 0.714251 0.48574 0.485679C0.714312 0.257108 0.980979 0.142822 1.28574 0.142822H12.7143C13.0191 0.142822 13.2857 0.257108 13.5143 0.485679C13.7429 0.714251 13.8572 0.980918 13.8572 1.28568V12.7143C13.8572 13.019 13.7429 13.2857 13.5143 13.5143C13.2857 13.7428 13.0191 13.8571 12.7143 13.8571H1.28574ZM1.28574 12.7143H12.7143V1.28568H1.28574V12.7143Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
3
res/img/element-icons/room/composer/numbered_list.svg
Normal file
3
res/img/element-icons/room/composer/numbered_list.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="13" height="12" viewBox="0 0 13 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.33334 2.66663H12.3333C12.7 2.66663 13 2.36663 13 1.99996C13 1.63329 12.7 1.33329 12.3333 1.33329H4.33334C3.96668 1.33329 3.66668 1.63329 3.66668 1.99996C3.66668 2.36663 3.96668 2.66663 4.33334 2.66663ZM12.3333 9.33329H4.33334C3.96668 9.33329 3.66668 9.63329 3.66668 9.99996C3.66668 10.3666 3.96668 10.6666 4.33334 10.6666H12.3333C12.7 10.6666 13 10.3666 13 9.99996C13 9.63329 12.7 9.33329 12.3333 9.33329ZM12.3333 5.33329H4.33334C3.96668 5.33329 3.66668 5.63329 3.66668 5.99996C3.66668 6.36663 3.96668 6.66663 4.33334 6.66663H12.3333C12.7 6.66663 13 6.36663 13 5.99996C13 5.63329 12.7 5.33329 12.3333 5.33329ZM2.00001 8.66663H0.666677C0.48001 8.66663 0.333344 8.81329 0.333344 8.99996C0.333344 9.18663 0.48001 9.33329 0.666677 9.33329H1.66668V9.66663H1.33334C1.14668 9.66663 1.00001 9.81329 1.00001 9.99996C1.00001 10.1866 1.14668 10.3333 1.33334 10.3333H1.66668V10.6666H0.666677C0.48001 10.6666 0.333344 10.8133 0.333344 11C0.333344 11.1866 0.48001 11.3333 0.666677 11.3333H2.00001C2.18668 11.3333 2.33334 11.1866 2.33334 11V8.99996C2.33334 8.81329 2.18668 8.66663 2.00001 8.66663ZM0.666677 1.33329H1.00001V2.99996C1.00001 3.18663 1.14668 3.33329 1.33334 3.33329C1.52001 3.33329 1.66668 3.18663 1.66668 2.99996V0.999959C1.66668 0.813293 1.52001 0.666626 1.33334 0.666626H0.666677C0.48001 0.666626 0.333344 0.813293 0.333344 0.999959C0.333344 1.18663 0.48001 1.33329 0.666677 1.33329ZM2.00001 4.66663H0.666677C0.48001 4.66663 0.333344 4.81329 0.333344 4.99996C0.333344 5.18663 0.48001 5.33329 0.666677 5.33329H1.53334L0.413343 6.63996C0.36001 6.69996 0.333344 6.77996 0.333344 6.85329V6.99996C0.333344 7.18663 0.48001 7.33329 0.666677 7.33329H2.00001C2.18668 7.33329 2.33334 7.18663 2.33334 6.99996C2.33334 6.81329 2.18668 6.66663 2.00001 6.66663H1.13334L2.25334 5.35996C2.30668 5.29996 2.33334 5.21996 2.33334 5.14663V4.99996C2.33334 4.81329 2.18668 4.66663 2.00001 4.66663Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2 KiB |
6
res/img/element-icons/room/composer/quote.svg
Normal file
6
res/img/element-icons/room/composer/quote.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="14" height="12" viewBox="0 0 14 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.1458 0.893371C2.20888 0.465499 1.90205 0.0690897 1.46046 0.00796516C1.01887 -0.0531594 0.609758 0.244148 0.546674 0.67202L0.00822047 4.32413C-0.0548633 4.752 0.251974 5.14841 0.69356 5.20954C1.13515 5.27066 1.54426 4.97336 1.60735 4.54548L2.1458 0.893371Z" fill="currentColor"/>
|
||||
<path d="M10.2226 7.67587C10.2857 7.248 9.97885 6.85159 9.53726 6.79046C9.09568 6.72934 8.68656 7.02664 8.62348 7.45452L8.08502 11.1066C8.02194 11.5345 8.32878 11.9309 8.77036 11.992C9.21195 12.0532 9.62107 11.7559 9.68415 11.328L10.2226 7.67587Z" fill="currentColor"/>
|
||||
<path d="M5.21224 0.00574343C5.65509 0.0575575 5.97074 0.447414 5.91727 0.876513L5.90255 0.993287C5.89309 1.06788 5.87936 1.17541 5.86224 1.30757C5.828 1.57178 5.78013 1.93492 5.72561 2.33035C5.6179 3.11153 5.48009 4.04989 5.36895 4.58829C5.28147 5.01211 4.85597 5.28697 4.41856 5.20221C3.98115 5.11744 3.69748 4.70515 3.78496 4.28133C3.88411 3.80099 4.01552 2.91329 4.12447 2.12309C4.17828 1.73284 4.22559 1.37397 4.25946 1.11259C4.27639 0.981947 4.28994 0.875787 4.29925 0.802389L4.31351 0.689266C4.36698 0.260167 4.76938 -0.0460706 5.21224 0.00574343Z" fill="currentColor"/>
|
||||
<path d="M13.9918 7.67587C14.0549 7.248 13.748 6.85159 13.3064 6.79046C12.8649 6.72934 12.4557 7.02664 12.3927 7.45452L11.8542 11.1066C11.7911 11.5345 12.098 11.9309 12.5395 11.992C12.9811 12.0532 13.3902 11.7559 13.4533 11.328L13.9918 7.67587Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
5
res/img/element-icons/settings/devices.svg
Normal file
5
res/img/element-icons/settings/devices.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="21" height="19" viewBox="0 0 21 19" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 2V7.005L18 7.003V4H2V14H13V16H2C1.45 16 0.979333 15.8043 0.588 15.413C0.196 15.021 0 14.55 0 14V2C0 1.45 0.196 0.979333 0.588 0.588C0.979333 0.196 1.45 0 2 0H18C18.55 0 19.021 0.196 19.413 0.588C19.8043 0.979333 20 1.45 20 2Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M15 8L20 8.005C20.55 8.005 21 8.45 21 9V18C21 18.55 20.55 19 20
|
||||
19H15C14.45 19 14 18.55 14 18V9C14 8.45 14.45 8 15 8ZM15 17H20V10H15V17Z" fill="currentColor"/>
|
||||
</svg>
|
After Width: | Height: | Size: 574 B |
|
@ -224,6 +224,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
|
|||
$breadcrumb-placeholder-bg-color: #272c35;
|
||||
$theme-button-bg-color: #e3e8f0;
|
||||
$resend-button-divider-color: rgba($header-panel-text-primary-color, 0.74);
|
||||
$inlinecode-border-color: $quinary-content;
|
||||
$inlinecode-background-color: $system;
|
||||
$codeblock-background-color: #2a3039;
|
||||
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
|
||||
$selected-color: $room-highlight-color;
|
||||
|
|
|
@ -190,6 +190,8 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
$composer-shadow-color: tranparent;
|
||||
|
||||
$codeblock-background-color: #2a3039;
|
||||
$inlinecode-border-color: #2a3039;
|
||||
$inlinecode-background-color: #2a3039;
|
||||
|
||||
/* Bubble tiles */
|
||||
$eventbubble-self-bg: #14322e;
|
||||
|
|
|
@ -290,6 +290,8 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
$composer-shadow-color: tranparent;
|
||||
|
||||
$codeblock-background-color: $header-panel-bg-color;
|
||||
$inlinecode-border-color: $header-panel-bg-color;
|
||||
$inlinecode-background-color: $header-panel-bg-color;
|
||||
|
||||
/* Bubble tiles */
|
||||
$eventbubble-self-bg: #f0fbf8;
|
||||
|
|
|
@ -295,6 +295,8 @@ $composer-shadow-color: rgba(0, 0, 0, 0.04);
|
|||
$breadcrumb-placeholder-bg-color: #e8eef5;
|
||||
$theme-button-bg-color: $quinary-content;
|
||||
$resend-button-divider-color: $input-darker-bg-color;
|
||||
$inlinecode-border-color: $quinary-content;
|
||||
$inlinecode-background-color: $system;
|
||||
$codeblock-background-color: $header-panel-bg-color;
|
||||
$scrollbar-thumb-color: rgba(0, 0, 0, 0.2);
|
||||
$selected-color: $secondary-accent-color;
|
||||
|
|
8
src/@types/diff-dom.d.ts
vendored
8
src/@types/diff-dom.d.ts
vendored
|
@ -20,10 +20,10 @@ declare module "diff-dom" {
|
|||
name: string;
|
||||
text?: string;
|
||||
route: number[];
|
||||
value: string;
|
||||
element: unknown;
|
||||
oldValue: string;
|
||||
newValue: string;
|
||||
value: HTMLElement | string;
|
||||
element: HTMLElement | string;
|
||||
oldValue: HTMLElement | string;
|
||||
newValue: HTMLElement | string;
|
||||
}
|
||||
|
||||
interface IOpts {}
|
||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
// This is intended to fix re-resizer because of its unguarded `instanceof TouchEvent` checks.
|
||||
export function polyfillTouchEvent() {
|
||||
export function polyfillTouchEvent(): void {
|
||||
// Firefox doesn't have touch events without touch devices being present, so create a fake
|
||||
// one we can rely on lying about.
|
||||
if (!window.TouchEvent) {
|
||||
|
|
|
@ -47,7 +47,7 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
|
|||
error: null,
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/element-web/issues/3148
|
||||
logger.log("Starting load of AsyncWrapper for modal");
|
||||
|
@ -69,15 +69,15 @@ export default class AsyncWrapper extends React.Component<IProps, IState> {
|
|||
});
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
private onWrapperCancelClick = () => {
|
||||
private onWrapperCancelClick = (): void => {
|
||||
this.props.onFinished(false);
|
||||
};
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
if (this.state.component) {
|
||||
const Component = this.state.component;
|
||||
return <Component {...this.props} />;
|
||||
|
|
|
@ -137,7 +137,12 @@ export function getInitialLetter(name: string): string {
|
|||
return split(name, "", 1)[0].toUpperCase();
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
export function avatarUrlForRoom(
|
||||
room: Room,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod?: ResizeMethod,
|
||||
): string | null {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
if (room.getMxcAvatarUrl()) {
|
||||
|
|
|
@ -272,7 +272,7 @@ export default abstract class BasePlatform {
|
|||
return null;
|
||||
}
|
||||
|
||||
public setLanguage(preferredLangs: string[]) {}
|
||||
public setLanguage(preferredLangs: string[]): void {}
|
||||
|
||||
public setSpellCheckEnabled(enabled: boolean): void {}
|
||||
|
||||
|
@ -280,7 +280,7 @@ export default abstract class BasePlatform {
|
|||
return null;
|
||||
}
|
||||
|
||||
public setSpellCheckLanguages(preferredLangs: string[]) {}
|
||||
public setSpellCheckLanguages(preferredLangs: string[]): void {}
|
||||
|
||||
public getSpellCheckLanguages(): Promise<string[]> | null {
|
||||
return null;
|
||||
|
|
|
@ -40,7 +40,7 @@ export class BlurhashEncoder {
|
|||
this.worker.onmessage = this.onMessage;
|
||||
}
|
||||
|
||||
private onMessage = (ev: MessageEvent<IBlurhashWorkerResponse>) => {
|
||||
private onMessage = (ev: MessageEvent<IBlurhashWorkerResponse>): void => {
|
||||
const { seq, blurhash } = ev.data;
|
||||
const deferred = this.pendingDeferredMap.get(seq);
|
||||
if (deferred) {
|
||||
|
|
|
@ -68,16 +68,20 @@ interface IMediaConfig {
|
|||
* @param {File} imageFile The file to load in an image element.
|
||||
* @return {Promise} A promise that resolves with the html image element.
|
||||
*/
|
||||
async function loadImageElement(imageFile: File) {
|
||||
async function loadImageElement(imageFile: File): Promise<{
|
||||
width: number;
|
||||
height: number;
|
||||
img: HTMLImageElement;
|
||||
}> {
|
||||
// Load the file into an html element
|
||||
const img = new Image();
|
||||
const objectUrl = URL.createObjectURL(imageFile);
|
||||
const imgPromise = new Promise((resolve, reject) => {
|
||||
img.onload = function () {
|
||||
img.onload = function (): void {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = function (e) {
|
||||
img.onerror = function (e): void {
|
||||
reject(e);
|
||||
};
|
||||
});
|
||||
|
@ -185,13 +189,13 @@ function loadVideoElement(videoFile: File): Promise<HTMLVideoElement> {
|
|||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = function (ev) {
|
||||
reader.onload = function (ev): void {
|
||||
// Wait until we have enough data to thumbnail the first frame.
|
||||
video.onloadeddata = async function () {
|
||||
video.onloadeddata = async function (): Promise<void> {
|
||||
resolve(video);
|
||||
video.pause();
|
||||
};
|
||||
video.onerror = function (e) {
|
||||
video.onerror = function (e): void {
|
||||
reject(e);
|
||||
};
|
||||
|
||||
|
@ -206,7 +210,7 @@ function loadVideoElement(videoFile: File): Promise<HTMLVideoElement> {
|
|||
video.load();
|
||||
video.play();
|
||||
};
|
||||
reader.onerror = function (e) {
|
||||
reader.onerror = function (e): void {
|
||||
reject(e);
|
||||
};
|
||||
reader.readAsDataURL(videoFile);
|
||||
|
@ -253,10 +257,10 @@ function infoForVideoFile(
|
|||
function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
reader.onload = function (e): void {
|
||||
resolve(e.target.result as ArrayBuffer);
|
||||
};
|
||||
reader.onerror = function (e) {
|
||||
reader.onerror = function (e): void {
|
||||
reject(e);
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
|
@ -461,7 +465,7 @@ export default class ContentMessages {
|
|||
matrixClient: MatrixClient,
|
||||
replyToEvent: MatrixEvent | undefined,
|
||||
promBefore?: Promise<any>,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const fileName = file.name || _t("Attachment");
|
||||
const content: Omit<IMediaEventContent, "info"> & { info: Partial<IMediaEventInfo> } = {
|
||||
body: fileName,
|
||||
|
@ -491,7 +495,7 @@ export default class ContentMessages {
|
|||
this.inprogress.push(upload);
|
||||
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
|
||||
|
||||
function onProgress(progress: UploadProgress) {
|
||||
function onProgress(progress: UploadProgress): void {
|
||||
upload.onProgress(progress);
|
||||
dis.dispatch<UploadProgressPayload>({ action: Action.UploadProgress, upload });
|
||||
}
|
||||
|
@ -568,7 +572,7 @@ export default class ContentMessages {
|
|||
}
|
||||
}
|
||||
|
||||
private isFileSizeAcceptable(file: File) {
|
||||
private isFileSizeAcceptable(file: File): boolean {
|
||||
if (
|
||||
this.mediaConfig !== null &&
|
||||
this.mediaConfig["m.upload.size"] !== undefined &&
|
||||
|
@ -599,7 +603,7 @@ export default class ContentMessages {
|
|||
});
|
||||
}
|
||||
|
||||
public static sharedInstance() {
|
||||
public static sharedInstance(): ContentMessages {
|
||||
if (window.mxContentMessages === undefined) {
|
||||
window.mxContentMessages = new ContentMessages();
|
||||
}
|
||||
|
|
|
@ -188,7 +188,7 @@ export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): bo
|
|||
return prevEventDate.getDay() !== nextEventDate.getDay();
|
||||
}
|
||||
|
||||
export function formatFullDateNoDay(date: Date) {
|
||||
export function formatFullDateNoDay(date: Date): string {
|
||||
return _t("%(date)s at %(time)s", {
|
||||
date: date.toLocaleDateString().replace(/\//g, "-"),
|
||||
time: date.toLocaleTimeString().replace(/:/g, "-"),
|
||||
|
@ -205,7 +205,7 @@ export function formatFullDateNoDayISO(date: Date): string {
|
|||
return date.toISOString();
|
||||
}
|
||||
|
||||
export function formatFullDateNoDayNoTime(date: Date) {
|
||||
export function formatFullDateNoDayNoTime(date: Date): string {
|
||||
return date.getFullYear() + "/" + pad(date.getMonth() + 1) + "/" + pad(date.getDate());
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||
import { ClientEvent, EventType, RoomStateEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||
import { IKeyBackupInfo } from "matrix-js-sdk/src/crypto/keybackup";
|
||||
|
||||
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
|
@ -56,7 +57,7 @@ export default class DeviceListener {
|
|||
// has the user dismissed any of the various nag toasts to setup encryption on this device?
|
||||
private dismissedThisDeviceToast = false;
|
||||
// cache of the key backup info
|
||||
private keyBackupInfo: object = null;
|
||||
private keyBackupInfo: IKeyBackupInfo | null = null;
|
||||
private keyBackupFetchedAt: number = null;
|
||||
private keyBackupStatusChecked = false;
|
||||
// We keep a list of our own device IDs so we can batch ones that were already
|
||||
|
@ -70,12 +71,12 @@ export default class DeviceListener {
|
|||
private enableBulkUnverifiedSessionsReminder = true;
|
||||
private deviceClientInformationSettingWatcherRef: string | undefined;
|
||||
|
||||
public static sharedInstance() {
|
||||
public static sharedInstance(): DeviceListener {
|
||||
if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener();
|
||||
return window.mxDeviceListener;
|
||||
}
|
||||
|
||||
public start() {
|
||||
public start(): void {
|
||||
this.running = true;
|
||||
MatrixClientPeg.get().on(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
|
||||
MatrixClientPeg.get().on(CryptoEvent.DevicesUpdated, this.onDevicesUpdated);
|
||||
|
@ -98,7 +99,7 @@ export default class DeviceListener {
|
|||
this.updateClientInformation();
|
||||
}
|
||||
|
||||
public stop() {
|
||||
public stop(): void {
|
||||
this.running = false;
|
||||
if (MatrixClientPeg.get()) {
|
||||
MatrixClientPeg.get().removeListener(CryptoEvent.WillUpdateDevices, this.onWillUpdateDevices);
|
||||
|
@ -134,7 +135,7 @@ export default class DeviceListener {
|
|||
*
|
||||
* @param {String[]} deviceIds List of device IDs to dismiss notifications for
|
||||
*/
|
||||
public async dismissUnverifiedSessions(deviceIds: Iterable<string>) {
|
||||
public async dismissUnverifiedSessions(deviceIds: Iterable<string>): Promise<void> {
|
||||
logger.log("Dismissing unverified sessions: " + Array.from(deviceIds).join(","));
|
||||
for (const d of deviceIds) {
|
||||
this.dismissed.add(d);
|
||||
|
@ -143,19 +144,19 @@ export default class DeviceListener {
|
|||
this.recheck();
|
||||
}
|
||||
|
||||
public dismissEncryptionSetup() {
|
||||
public dismissEncryptionSetup(): void {
|
||||
this.dismissedThisDeviceToast = true;
|
||||
this.recheck();
|
||||
}
|
||||
|
||||
private ensureDeviceIdsAtStartPopulated() {
|
||||
private ensureDeviceIdsAtStartPopulated(): void {
|
||||
if (this.ourDeviceIdsAtStart === null) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
this.ourDeviceIdsAtStart = new Set(cli.getStoredDevicesForUser(cli.getUserId()).map((d) => d.deviceId));
|
||||
}
|
||||
}
|
||||
|
||||
private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
|
||||
private onWillUpdateDevices = async (users: string[], initialFetch?: boolean): Promise<void> => {
|
||||
// If we didn't know about *any* devices before (ie. it's fresh login),
|
||||
// then they are all pre-existing devices, so ignore this and set the
|
||||
// devicesAtStart list to the devices that we see after the fetch.
|
||||
|
@ -168,26 +169,26 @@ export default class DeviceListener {
|
|||
// before we download any new ones.
|
||||
};
|
||||
|
||||
private onDevicesUpdated = (users: string[]) => {
|
||||
private onDevicesUpdated = (users: string[]): void => {
|
||||
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onDeviceVerificationChanged = (userId: string) => {
|
||||
private onDeviceVerificationChanged = (userId: string): void => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onUserTrustStatusChanged = (userId: string) => {
|
||||
private onUserTrustStatusChanged = (userId: string): void => {
|
||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onCrossSingingKeysChanged = () => {
|
||||
private onCrossSingingKeysChanged = (): void => {
|
||||
this.recheck();
|
||||
};
|
||||
|
||||
private onAccountData = (ev: MatrixEvent) => {
|
||||
private onAccountData = (ev: MatrixEvent): void => {
|
||||
// User may have:
|
||||
// * migrated SSSS to symmetric
|
||||
// * uploaded keys to secret storage
|
||||
|
@ -202,13 +203,13 @@ export default class DeviceListener {
|
|||
}
|
||||
};
|
||||
|
||||
private onSync = (state: SyncState, prevState?: SyncState) => {
|
||||
private onSync = (state: SyncState, prevState?: SyncState): void => {
|
||||
if (state === "PREPARED" && prevState === null) {
|
||||
this.recheck();
|
||||
}
|
||||
};
|
||||
|
||||
private onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
private onRoomStateEvents = (ev: MatrixEvent): void => {
|
||||
if (ev.getType() !== EventType.RoomEncryption) return;
|
||||
|
||||
// If a room changes to encrypted, re-check as it may be our first
|
||||
|
@ -216,7 +217,7 @@ export default class DeviceListener {
|
|||
this.recheck();
|
||||
};
|
||||
|
||||
private onAction = ({ action }: ActionPayload) => {
|
||||
private onAction = ({ action }: ActionPayload): void => {
|
||||
if (action !== Action.OnLoggedIn) return;
|
||||
this.recheck();
|
||||
this.updateClientInformation();
|
||||
|
@ -224,7 +225,7 @@ export default class DeviceListener {
|
|||
|
||||
// The server doesn't tell us when key backup is set up, so we poll
|
||||
// & cache the result
|
||||
private async getKeyBackupInfo() {
|
||||
private async getKeyBackupInfo(): Promise<IKeyBackupInfo> {
|
||||
const now = new Date().getTime();
|
||||
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
|
||||
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
|
@ -233,7 +234,7 @@ export default class DeviceListener {
|
|||
return this.keyBackupInfo;
|
||||
}
|
||||
|
||||
private shouldShowSetupEncryptionToast() {
|
||||
private shouldShowSetupEncryptionToast(): boolean {
|
||||
// If we're in the middle of a secret storage operation, we're likely
|
||||
// modifying the state involved here, so don't add new toasts to setup.
|
||||
if (isSecretStorageBeingAccessed()) return false;
|
||||
|
@ -242,7 +243,7 @@ export default class DeviceListener {
|
|||
return cli && cli.getRooms().some((r) => cli.isRoomEncrypted(r.roomId));
|
||||
}
|
||||
|
||||
private async recheck() {
|
||||
private async recheck(): Promise<void> {
|
||||
if (!this.running) return; // we have been stopped
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
|
@ -359,7 +360,7 @@ export default class DeviceListener {
|
|||
this.displayingToastsForDeviceIds = newUnverifiedDeviceIds;
|
||||
}
|
||||
|
||||
private checkKeyBackupStatus = async () => {
|
||||
private checkKeyBackupStatus = async (): Promise<void> => {
|
||||
if (this.keyBackupStatusChecked) {
|
||||
return;
|
||||
}
|
||||
|
@ -388,7 +389,7 @@ export default class DeviceListener {
|
|||
}
|
||||
};
|
||||
|
||||
private updateClientInformation = async () => {
|
||||
private updateClientInformation = async (): Promise<void> => {
|
||||
try {
|
||||
if (this.shouldRecordClientInformation) {
|
||||
await recordClientInformation(MatrixClientPeg.get(), SdkConfig.get(), PlatformPeg.get());
|
||||
|
|
|
@ -16,5 +16,6 @@ limitations under the License.
|
|||
|
||||
import { TimelineRenderingType } from "./contexts/RoomContext";
|
||||
|
||||
export const editorRoomKey = (roomId: string, context: TimelineRenderingType) => `mx_edit_room_${roomId}_${context}`;
|
||||
export const editorStateKey = (eventId: string) => `mx_edit_state_${eventId}`;
|
||||
export const editorRoomKey = (roomId: string, context: TimelineRenderingType): string =>
|
||||
`mx_edit_room_${roomId}_${context}`;
|
||||
export const editorStateKey = (eventId: string): string => `mx_edit_state_${eventId}`;
|
||||
|
|
|
@ -449,9 +449,9 @@ export interface IOptsReturnString extends IOpts {
|
|||
returnString: true;
|
||||
}
|
||||
|
||||
const emojiToHtmlSpan = (emoji: string) =>
|
||||
const emojiToHtmlSpan = (emoji: string): string =>
|
||||
`<span class='mx_Emoji' title='${unicodeToShortcode(emoji)}'>${emoji}</span>`;
|
||||
const emojiToJsxSpan = (emoji: string, key: number) => (
|
||||
const emojiToJsxSpan = (emoji: string, key: number): JSX.Element => (
|
||||
<span key={key} className="mx_Emoji" title={unicodeToShortcode(emoji)}>
|
||||
{emoji}
|
||||
</span>
|
||||
|
@ -505,7 +505,7 @@ function formatEmojis(message: string, isHtmlMessage: boolean): (JSX.Element | s
|
|||
*/
|
||||
export function bodyToHtml(content: IContent, highlights: Optional<string[]>, opts: IOptsReturnString): string;
|
||||
export function bodyToHtml(content: IContent, highlights: Optional<string[]>, opts: IOptsReturnNode): ReactNode;
|
||||
export function bodyToHtml(content: IContent, highlights: Optional<string[]>, opts: IOpts = {}) {
|
||||
export function bodyToHtml(content: IContent, highlights: Optional<string[]>, opts: IOpts = {}): ReactNode | string {
|
||||
const isFormattedBody = content.format === "org.matrix.custom.html" && !!content.formatted_body;
|
||||
let bodyHasEmoji = false;
|
||||
let isHtmlMessage = false;
|
||||
|
|
|
@ -99,7 +99,7 @@ export interface IConfigOptions {
|
|||
features?: Record<string, boolean>; // <FeatureName, EnabledBool>
|
||||
|
||||
bug_report_endpoint_url?: string; // omission disables bug reporting
|
||||
uisi_autorageshake_app?: string;
|
||||
uisi_autorageshake_app?: string; // defaults to "element-auto-uisi"
|
||||
sentry?: {
|
||||
dsn: string;
|
||||
environment?: string; // "production", etc
|
||||
|
|
|
@ -28,7 +28,7 @@ limitations under the License.
|
|||
* consume in the timeline, when performing scroll offset calculations
|
||||
* (e.g. scroll locking)
|
||||
*/
|
||||
export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
|
||||
export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number): number {
|
||||
if (!fullWidth || !fullHeight) {
|
||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||
// log this because it's spammy
|
||||
|
|
|
@ -76,7 +76,7 @@ export const Key = {
|
|||
|
||||
export const IS_MAC = navigator.platform.toUpperCase().includes("MAC");
|
||||
|
||||
export function isOnlyCtrlOrCmdKeyEvent(ev) {
|
||||
export function isOnlyCtrlOrCmdKeyEvent(ev: KeyboardEvent): boolean {
|
||||
if (IS_MAC) {
|
||||
return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey;
|
||||
} else {
|
||||
|
|
|
@ -169,7 +169,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
|
||||
private silencedCalls = new Set<string>(); // callIds
|
||||
|
||||
public static get instance() {
|
||||
public static get instance(): LegacyCallHandler {
|
||||
if (!window.mxLegacyCallHandler) {
|
||||
window.mxLegacyCallHandler = new LegacyCallHandler();
|
||||
}
|
||||
|
@ -456,7 +456,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
return callsNotInThatRoom;
|
||||
}
|
||||
|
||||
public getAllActiveCallsForPip(roomId: string) {
|
||||
public getAllActiveCallsForPip(roomId: string): MatrixCall[] {
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
if (WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
|
||||
// This checks if there is space for the call view in the aux panel
|
||||
|
@ -478,7 +478,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||
if (audio) {
|
||||
this.addEventListenersForAudioElement(audio);
|
||||
const playAudio = async () => {
|
||||
const playAudio = async (): Promise<void> => {
|
||||
try {
|
||||
if (audio.muted) {
|
||||
logger.error(
|
||||
|
@ -524,7 +524,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||
const pauseAudio = () => {
|
||||
const pauseAudio = (): void => {
|
||||
logger.debug(`${logPrefix} pausing audio`);
|
||||
// pause doesn't return a promise, so just do it
|
||||
audio.pause();
|
||||
|
@ -600,7 +600,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
this.setCallListeners(newCall);
|
||||
this.setCallState(newCall, newCall.state);
|
||||
});
|
||||
call.on(CallEvent.AssertedIdentityChanged, async () => {
|
||||
call.on(CallEvent.AssertedIdentityChanged, async (): Promise<void> => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
logger.log(`Call ID ${call.callId} got new asserted identity:`, call.getRemoteAssertedIdentity());
|
||||
|
@ -808,7 +808,7 @@ export default class LegacyCallHandler extends EventEmitter {
|
|||
|
||||
private showICEFallbackPrompt(): void {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const code = (sub) => <code>{sub}</code>;
|
||||
const code = (sub: string): JSX.Element => <code>{sub}</code>;
|
||||
Modal.createDialog(
|
||||
QuestionDialog,
|
||||
{
|
||||
|
|
|
@ -219,7 +219,7 @@ export function attemptTokenLogin(
|
|||
})
|
||||
.then(function (creds) {
|
||||
logger.log("Logged in with token");
|
||||
return clearStorage().then(async () => {
|
||||
return clearStorage().then(async (): Promise<boolean> => {
|
||||
await persistCredentials(creds);
|
||||
// remember that we just logged in
|
||||
sessionStorage.setItem("mx_fresh_login", String(true));
|
||||
|
@ -406,7 +406,7 @@ async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
|
|||
);
|
||||
}
|
||||
|
||||
async function abortLogin() {
|
||||
async function abortLogin(): Promise<void> {
|
||||
const signOut = await showStorageEvictedDialog();
|
||||
if (signOut) {
|
||||
await clearStorage();
|
||||
|
|
|
@ -20,14 +20,14 @@ import { MatrixClientPeg } from "./MatrixClientPeg";
|
|||
import SdkConfig from "./SdkConfig";
|
||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||
|
||||
export function getConfigLivestreamUrl() {
|
||||
export function getConfigLivestreamUrl(): string | undefined {
|
||||
return SdkConfig.get("audio_stream_url");
|
||||
}
|
||||
|
||||
// Dummy rtmp URL used to signal that we want a special audio-only stream
|
||||
const AUDIOSTREAM_DUMMY_URL = "rtmp://audiostream.dummy/";
|
||||
|
||||
async function createLiveStream(roomId: string) {
|
||||
async function createLiveStream(roomId: string): Promise<void> {
|
||||
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
|
||||
const url = getConfigLivestreamUrl() + "/createStream";
|
||||
|
@ -47,7 +47,7 @@ async function createLiveStream(roomId: string) {
|
|||
return respBody["stream_id"];
|
||||
}
|
||||
|
||||
export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {
|
||||
export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string): Promise<void> {
|
||||
const streamId = await createLiveStream(roomId);
|
||||
|
||||
await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
|
||||
|
|
|
@ -122,7 +122,7 @@ export default class Login {
|
|||
initial_device_display_name: this.defaultDeviceDisplayName,
|
||||
};
|
||||
|
||||
const tryFallbackHs = (originalError) => {
|
||||
const tryFallbackHs = (originalError: Error): Promise<IMatrixClientCreds> => {
|
||||
return sendLoginRequest(this.fallbackHsUrl, this.isUrl, "m.login.password", loginParams).catch(
|
||||
(fallbackError) => {
|
||||
logger.log("fallback HS login failed", fallbackError);
|
||||
|
|
|
@ -56,7 +56,7 @@ function isMultiLine(node: commonmark.Node): boolean {
|
|||
return par.firstChild != par.lastChild;
|
||||
}
|
||||
|
||||
function getTextUntilEndOrLinebreak(node: commonmark.Node) {
|
||||
function getTextUntilEndOrLinebreak(node: commonmark.Node): string {
|
||||
let currentNode = node;
|
||||
let text = "";
|
||||
while (currentNode !== null && currentNode.type !== "softbreak" && currentNode.type !== "linebreak") {
|
||||
|
@ -137,7 +137,7 @@ export default class Markdown {
|
|||
* See: https://github.com/vector-im/element-web/issues/4674
|
||||
* @param parsed
|
||||
*/
|
||||
private repairLinks(parsed: commonmark.Node) {
|
||||
private repairLinks(parsed: commonmark.Node): commonmark.Node {
|
||||
const walker = parsed.walker();
|
||||
let event: commonmark.NodeWalkingStep = null;
|
||||
let text = "";
|
||||
|
|
|
@ -77,7 +77,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
// Neither the static nor priority modal will be in this list.
|
||||
private modals: IModal<any>[] = [];
|
||||
|
||||
private static getOrCreateContainer() {
|
||||
private static getOrCreateContainer(): HTMLElement {
|
||||
let container = document.getElementById(DIALOG_CONTAINER_ID);
|
||||
|
||||
if (!container) {
|
||||
|
@ -89,7 +89,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
return container;
|
||||
}
|
||||
|
||||
private static getOrCreateStaticContainer() {
|
||||
private static getOrCreateStaticContainer(): HTMLElement {
|
||||
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
|
||||
|
||||
if (!container) {
|
||||
|
@ -101,31 +101,31 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
return container;
|
||||
}
|
||||
|
||||
public toggleCurrentDialogVisibility() {
|
||||
public toggleCurrentDialogVisibility(): void {
|
||||
const modal = this.getCurrentModal();
|
||||
if (!modal) return;
|
||||
modal.hidden = !modal.hidden;
|
||||
}
|
||||
|
||||
public hasDialogs() {
|
||||
return this.priorityModal || this.staticModal || this.modals.length > 0;
|
||||
public hasDialogs(): boolean {
|
||||
return !!this.priorityModal || !!this.staticModal || this.modals.length > 0;
|
||||
}
|
||||
|
||||
public createDialog<T extends any[]>(
|
||||
Element: React.ComponentType<any>,
|
||||
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]>
|
||||
) {
|
||||
): IHandle<T> {
|
||||
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest);
|
||||
}
|
||||
|
||||
public appendDialog<T extends any[]>(
|
||||
Element: React.ComponentType,
|
||||
...rest: ParametersWithoutFirst<ModalManager["appendDialogAsync"]>
|
||||
) {
|
||||
): IHandle<T> {
|
||||
return this.appendDialogAsync<T>(Promise.resolve(Element), ...rest);
|
||||
}
|
||||
|
||||
public closeCurrentModal(reason: string) {
|
||||
public closeCurrentModal(reason: string): void {
|
||||
const modal = this.getCurrentModal();
|
||||
if (!modal) {
|
||||
return;
|
||||
|
@ -139,7 +139,11 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
props?: IProps<T>,
|
||||
className?: string,
|
||||
options?: IOptions<T>,
|
||||
) {
|
||||
): {
|
||||
modal: IModal<T>;
|
||||
closeDialog: IHandle<T>["close"];
|
||||
onFinishedProm: IHandle<T>["finished"];
|
||||
} {
|
||||
const modal: IModal<T> = {
|
||||
onFinished: props ? props.onFinished : null,
|
||||
onBeforeClose: options.onBeforeClose,
|
||||
|
@ -173,7 +177,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
): [IHandle<T>["close"], IHandle<T>["finished"]] {
|
||||
const deferred = defer<T>();
|
||||
return [
|
||||
async (...args: T) => {
|
||||
async (...args: T): Promise<void> => {
|
||||
if (modal.beforeClosePromise) {
|
||||
await modal.beforeClosePromise;
|
||||
} else if (modal.onBeforeClose) {
|
||||
|
@ -302,7 +306,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
}
|
||||
}
|
||||
|
||||
private onBackgroundClick = () => {
|
||||
private onBackgroundClick = (): void => {
|
||||
const modal = this.getCurrentModal();
|
||||
if (!modal) {
|
||||
return;
|
||||
|
@ -320,7 +324,7 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
|
|||
return this.priorityModal ? this.priorityModal : this.modals[0] || this.staticModal;
|
||||
}
|
||||
|
||||
private async reRender() {
|
||||
private async reRender(): Promise<void> {
|
||||
// await next tick because sometimes ReactDOM can race with itself and cause the modal to wrongly stick around
|
||||
await sleep(0);
|
||||
|
||||
|
|
|
@ -50,7 +50,8 @@ import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded
|
|||
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
|
||||
import ToastStore from "./stores/ToastStore";
|
||||
import { ElementCall } from "./models/Call";
|
||||
import { VoiceBroadcastChunkEventType } from "./voice-broadcast";
|
||||
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
|
||||
import { getSenderName } from "./utils/event/getSenderName";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -80,9 +81,16 @@ const msgTypeHandlers = {
|
|||
},
|
||||
[MsgType.Audio]: (event: MatrixEvent): string | null => {
|
||||
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
|
||||
// mute broadcast chunks
|
||||
if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
|
||||
// Show a notification for the first broadcast chunk.
|
||||
// At this point a user received something to listen to.
|
||||
return _t("%(senderName)s started a voice broadcast", { senderName: getSenderName(event) });
|
||||
}
|
||||
|
||||
// Mute other broadcast chunks
|
||||
return null;
|
||||
}
|
||||
|
||||
return TextForEvent.textForEvent(event);
|
||||
},
|
||||
};
|
||||
|
@ -448,6 +456,9 @@ export const Notifier = {
|
|||
},
|
||||
|
||||
_evaluateEvent: function (ev: MatrixEvent) {
|
||||
// Mute notifications for broadcast info events
|
||||
if (ev.getType() === VoiceBroadcastInfoEventType) return;
|
||||
|
||||
let roomId = ev.getRoomId();
|
||||
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
|
||||
// Attempt to translate a virtual room to a native one
|
||||
|
|
|
@ -104,6 +104,10 @@ export default class PasswordReset {
|
|||
);
|
||||
}
|
||||
|
||||
public setLogoutDevices(logoutDevices: boolean): void {
|
||||
this.logoutDevices = logoutDevices;
|
||||
}
|
||||
|
||||
public async setNewPassword(password: string): Promise<void> {
|
||||
this.password = password;
|
||||
await this.checkEmailLinkClicked();
|
||||
|
|
|
@ -32,13 +32,13 @@ import { PlatformSetPayload } from "./dispatcher/payloads/PlatformSetPayload";
|
|||
* object.
|
||||
*/
|
||||
export class PlatformPeg {
|
||||
private platform: BasePlatform = null;
|
||||
private platform: BasePlatform | null = null;
|
||||
|
||||
/**
|
||||
* Returns the current Platform object for the application.
|
||||
* This should be an instance of a class extending BasePlatform.
|
||||
*/
|
||||
public get() {
|
||||
public get(): BasePlatform | null {
|
||||
return this.platform;
|
||||
}
|
||||
|
||||
|
@ -46,7 +46,7 @@ export class PlatformPeg {
|
|||
* Sets the current platform handler object to use for the application.
|
||||
* @param {BasePlatform} platform an instance of a class extending BasePlatform.
|
||||
*/
|
||||
public set(platform: BasePlatform) {
|
||||
public set(platform: BasePlatform): void {
|
||||
this.platform = platform;
|
||||
defaultDispatcher.dispatch<PlatformSetPayload>({
|
||||
action: Action.PlatformSet,
|
||||
|
|
|
@ -175,7 +175,7 @@ export class PosthogAnalytics {
|
|||
this.onLayoutUpdated();
|
||||
}
|
||||
|
||||
private onLayoutUpdated = () => {
|
||||
private onLayoutUpdated = (): void => {
|
||||
let layout: UserProperties["WebLayout"];
|
||||
|
||||
switch (SettingsStore.getValue("layout")) {
|
||||
|
@ -195,7 +195,7 @@ export class PosthogAnalytics {
|
|||
this.setProperty("WebLayout", layout);
|
||||
};
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action !== Action.SettingUpdated) return;
|
||||
const settingsPayload = payload as SettingUpdatedPayload;
|
||||
if (["layout", "useCompactLayout"].includes(settingsPayload.settingName)) {
|
||||
|
@ -232,7 +232,7 @@ export class PosthogAnalytics {
|
|||
return properties;
|
||||
};
|
||||
|
||||
private registerSuperProperties(properties: Properties) {
|
||||
private registerSuperProperties(properties: Properties): void {
|
||||
if (this.enabled) {
|
||||
this.posthog.register(properties);
|
||||
}
|
||||
|
@ -255,7 +255,7 @@ export class PosthogAnalytics {
|
|||
}
|
||||
|
||||
// eslint-disable-nextline no-unused-varsx
|
||||
private capture(eventName: string, properties: Properties, options?: IPostHogEventOptions) {
|
||||
private capture(eventName: string, properties: Properties, options?: IPostHogEventOptions): void {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -107,20 +107,20 @@ export default class PosthogTrackers {
|
|||
}
|
||||
|
||||
export class PosthogScreenTracker extends PureComponent<{ screenName: ScreenName }> {
|
||||
public componentDidMount() {
|
||||
public componentDidMount(): void {
|
||||
PosthogTrackers.instance.trackOverride(this.props.screenName);
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
public componentDidUpdate(): void {
|
||||
// We do not clear the old override here so that we do not send the non-override screen as a transition
|
||||
PosthogTrackers.instance.trackOverride(this.props.screenName);
|
||||
}
|
||||
|
||||
public componentWillUnmount() {
|
||||
public componentWillUnmount(): void {
|
||||
PosthogTrackers.instance.clearOverride(this.props.screenName);
|
||||
}
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
return null; // no need to render anything, we just need to hook into the React lifecycle
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ class Presence {
|
|||
* Start listening the user activity to evaluate his presence state.
|
||||
* Any state change will be sent to the homeserver.
|
||||
*/
|
||||
public async start() {
|
||||
public async start(): Promise<void> {
|
||||
this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
|
||||
// the user_activity_start action starts the timer
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
|
@ -58,7 +58,7 @@ class Presence {
|
|||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
public stop() {
|
||||
public stop(): void {
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.dispatcherRef = null;
|
||||
|
@ -73,11 +73,11 @@ class Presence {
|
|||
* Get the current presence state.
|
||||
* @returns {string} the presence state (see PRESENCE enum)
|
||||
*/
|
||||
public getState() {
|
||||
public getState(): State {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
private onAction = (payload: ActionPayload): void => {
|
||||
if (payload.action === "user_activity") {
|
||||
this.setState(State.Online);
|
||||
this.unavailableTimer.restart();
|
||||
|
@ -89,7 +89,7 @@ class Presence {
|
|||
* If the state has changed, the homeserver will be notified.
|
||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||
*/
|
||||
private async setState(newState: State) {
|
||||
private async setState(newState: State): Promise<void> {
|
||||
if (newState === this.state) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ export default class ScalarAuthClient {
|
|||
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
|
||||
}
|
||||
|
||||
private writeTokenToStore() {
|
||||
private writeTokenToStore(): void {
|
||||
window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
|
||||
if (this.isDefaultManager) {
|
||||
// We remove the old token from storage to migrate upwards. This is safe
|
||||
|
@ -72,7 +72,7 @@ export default class ScalarAuthClient {
|
|||
return this.readTokenFromStore();
|
||||
}
|
||||
|
||||
public setTermsInteractionCallback(callback) {
|
||||
public setTermsInteractionCallback(callback: TermsInteractionCallback): void {
|
||||
this.termsInteractionCallback = callback;
|
||||
}
|
||||
|
||||
|
|
|
@ -711,7 +711,7 @@ function returnStateEvent(event: MessageEvent<any>, roomId: string, eventType: s
|
|||
sendResponse(event, stateEvent.getContent());
|
||||
}
|
||||
|
||||
async function getOpenIdToken(event: MessageEvent<any>) {
|
||||
async function getOpenIdToken(event: MessageEvent<any>): Promise<void> {
|
||||
try {
|
||||
const tokenObject = await MatrixClientPeg.get().getOpenIdToken();
|
||||
sendResponse(event, tokenObject);
|
||||
|
@ -728,7 +728,7 @@ async function sendEvent(
|
|||
content?: IContent;
|
||||
}>,
|
||||
roomId: string,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const eventType = event.data.type;
|
||||
const stateKey = event.data.state_key;
|
||||
const content = event.data.content;
|
||||
|
@ -786,7 +786,7 @@ async function readEvents(
|
|||
limit?: number;
|
||||
}>,
|
||||
roomId: string,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const eventType = event.data.type;
|
||||
const stateKey = event.data.state_key;
|
||||
const limit = event.data.limit;
|
||||
|
|
|
@ -27,6 +27,8 @@ export const DEFAULTS: IConfigOptions = {
|
|||
integrations_ui_url: "https://scalar.vector.im/",
|
||||
integrations_rest_url: "https://scalar.vector.im/api",
|
||||
bug_report_endpoint_url: null,
|
||||
uisi_autorageshake_app: "element-auto-uisi",
|
||||
|
||||
jitsi: {
|
||||
preferred_domain: "meet.element.io",
|
||||
},
|
||||
|
@ -56,7 +58,7 @@ export default class SdkConfig {
|
|||
private static instance: IConfigOptions;
|
||||
private static fallback: SnakedObject<IConfigOptions>;
|
||||
|
||||
private static setInstance(i: IConfigOptions) {
|
||||
private static setInstance(i: IConfigOptions): void {
|
||||
SdkConfig.instance = i;
|
||||
SdkConfig.fallback = new SnakedObject(i);
|
||||
|
||||
|
@ -90,18 +92,18 @@ export default class SdkConfig {
|
|||
return val === undefined ? undefined : null;
|
||||
}
|
||||
|
||||
public static put(cfg: Partial<IConfigOptions>) {
|
||||
public static put(cfg: Partial<IConfigOptions>): void {
|
||||
SdkConfig.setInstance({ ...DEFAULTS, ...cfg });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the config to be completely empty.
|
||||
*/
|
||||
public static unset() {
|
||||
public static unset(): void {
|
||||
SdkConfig.setInstance(<IConfigOptions>{}); // safe to cast - defaults will be applied
|
||||
}
|
||||
|
||||
public static add(cfg: Partial<IConfigOptions>) {
|
||||
public static add(cfg: Partial<IConfigOptions>): void {
|
||||
SdkConfig.put({ ...SdkConfig.get(), ...cfg });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ async function confirmToDismiss(): Promise<boolean> {
|
|||
type KeyParams = { passphrase: string; recoveryKey: string };
|
||||
|
||||
function makeInputToKey(keyInfo: ISecretStorageKeyInfo): (keyParams: KeyParams) => Promise<Uint8Array> {
|
||||
return async ({ passphrase, recoveryKey }) => {
|
||||
return async ({ passphrase, recoveryKey }): Promise<Uint8Array> => {
|
||||
if (passphrase) {
|
||||
return deriveKey(passphrase, keyInfo.passphrase.salt, keyInfo.passphrase.iterations);
|
||||
} else {
|
||||
|
@ -151,7 +151,7 @@ async function getSecretStorageKey({
|
|||
/* props= */
|
||||
{
|
||||
keyInfo,
|
||||
checkPrivateKey: async (input: KeyParams) => {
|
||||
checkPrivateKey: async (input: KeyParams): Promise<boolean> => {
|
||||
const key = await inputToKey(input);
|
||||
return MatrixClientPeg.get().checkSecretStorageKey(key, keyInfo);
|
||||
},
|
||||
|
@ -160,7 +160,7 @@ async function getSecretStorageKey({
|
|||
/* isPriorityModal= */ false,
|
||||
/* isStaticModal= */ false,
|
||||
/* options= */ {
|
||||
onBeforeClose: async (reason) => {
|
||||
onBeforeClose: async (reason): Promise<boolean> => {
|
||||
if (reason === "backgroundClick") {
|
||||
return confirmToDismiss();
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ export async function getDehydrationKey(
|
|||
/* props= */
|
||||
{
|
||||
keyInfo,
|
||||
checkPrivateKey: async (input) => {
|
||||
checkPrivateKey: async (input): Promise<boolean> => {
|
||||
const key = await inputToKey(input);
|
||||
try {
|
||||
checkFunc(key);
|
||||
|
@ -210,7 +210,7 @@ export async function getDehydrationKey(
|
|||
/* isPriorityModal= */ false,
|
||||
/* isStaticModal= */ false,
|
||||
/* options= */ {
|
||||
onBeforeClose: async (reason) => {
|
||||
onBeforeClose: async (reason): Promise<boolean> => {
|
||||
if (reason === "backgroundClick") {
|
||||
return confirmToDismiss();
|
||||
}
|
||||
|
@ -324,7 +324,7 @@ export async function promptForBackupPassphrase(): Promise<Uint8Array> {
|
|||
* bootstrapped. Optional.
|
||||
* @param {bool} [forceReset] Reset secret storage even if it's already set up
|
||||
*/
|
||||
export async function accessSecretStorage(func = async () => {}, forceReset = false) {
|
||||
export async function accessSecretStorage(func = async (): Promise<void> => {}, forceReset = false): Promise<void> {
|
||||
const cli = MatrixClientPeg.get();
|
||||
secretStorageBeingAccessed = true;
|
||||
try {
|
||||
|
@ -342,7 +342,7 @@ export async function accessSecretStorage(func = async () => {}, forceReset = fa
|
|||
/* priority = */ false,
|
||||
/* static = */ true,
|
||||
/* options = */ {
|
||||
onBeforeClose: async (reason) => {
|
||||
onBeforeClose: async (reason): Promise<boolean> => {
|
||||
// If Secure Backup is required, you cannot leave the modal.
|
||||
if (reason === "backgroundClick") {
|
||||
return !isSecureBackupRequired();
|
||||
|
@ -357,7 +357,7 @@ export async function accessSecretStorage(func = async () => {}, forceReset = fa
|
|||
}
|
||||
} else {
|
||||
await cli.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
authUploadDeviceSigningKeys: async (makeRequest): Promise<void> => {
|
||||
const { finished } = Modal.createDialog(InteractiveAuthDialog, {
|
||||
title: _t("Setting up keys"),
|
||||
matrixClient: cli,
|
||||
|
|
|
@ -60,7 +60,7 @@ export default class SendHistoryManager {
|
|||
};
|
||||
}
|
||||
|
||||
public save(editorModel: EditorModel, replyEvent?: MatrixEvent) {
|
||||
public save(editorModel: EditorModel, replyEvent?: MatrixEvent): void {
|
||||
const item = SendHistoryManager.createItem(editorModel, replyEvent);
|
||||
this.history.push(item);
|
||||
this.currentIndex = this.history.length;
|
||||
|
|
|
@ -85,7 +85,7 @@ const singleMxcUpload = async (): Promise<string | null> => {
|
|||
|
||||
Modal.createDialog(UploadConfirmDialog, {
|
||||
file,
|
||||
onFinished: async (shouldContinue) => {
|
||||
onFinished: async (shouldContinue): Promise<void> => {
|
||||
if (shouldContinue) {
|
||||
const { content_uri: uri } = await MatrixClientPeg.get().uploadContent(file);
|
||||
resolve(uri);
|
||||
|
@ -151,11 +151,11 @@ export class Command {
|
|||
this.analyticsName = opts.analyticsName;
|
||||
}
|
||||
|
||||
public getCommand() {
|
||||
public getCommand(): string {
|
||||
return `/${this.command}`;
|
||||
}
|
||||
|
||||
public getCommandWithArgs() {
|
||||
public getCommandWithArgs(): string {
|
||||
return this.getCommand() + " " + this.args;
|
||||
}
|
||||
|
||||
|
@ -184,7 +184,7 @@ export class Command {
|
|||
return this.runFn(roomId, args);
|
||||
}
|
||||
|
||||
public getUsage() {
|
||||
public getUsage(): string {
|
||||
return _t("Usage") + ": " + this.getCommandWithArgs();
|
||||
}
|
||||
|
||||
|
@ -193,15 +193,15 @@ export class Command {
|
|||
}
|
||||
}
|
||||
|
||||
function reject(error) {
|
||||
function reject(error?: any): RunResult {
|
||||
return { error };
|
||||
}
|
||||
|
||||
function success(promise?: Promise<any>) {
|
||||
function success(promise?: Promise<any>): RunResult {
|
||||
return { promise };
|
||||
}
|
||||
|
||||
function successSync(value: any) {
|
||||
function successSync(value: any): RunResult {
|
||||
return success(Promise.resolve(value));
|
||||
}
|
||||
|
||||
|
@ -319,7 +319,7 @@ export const Commands = [
|
|||
);
|
||||
|
||||
return success(
|
||||
finished.then(async ([resp]) => {
|
||||
finished.then(async ([resp]): Promise<void> => {
|
||||
if (!resp?.continue) return;
|
||||
await upgradeRoom(room, args, resp.invite);
|
||||
}),
|
||||
|
@ -338,7 +338,7 @@ export const Commands = [
|
|||
runFn: function (roomId, args) {
|
||||
if (args) {
|
||||
return success(
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
const unixTimestamp = Date.parse(args);
|
||||
if (!unixTimestamp) {
|
||||
throw newTranslatableError(
|
||||
|
@ -501,7 +501,9 @@ export const Commands = [
|
|||
? ContentHelpers.parseTopicContent(content)
|
||||
: { text: _t("This room has no topic.") };
|
||||
|
||||
const ref = (e) => e && linkifyElement(e);
|
||||
const ref = (e): void => {
|
||||
if (e) linkifyElement(e);
|
||||
};
|
||||
const body = topicToHtml(topic.text, topic.html, ref, true);
|
||||
|
||||
Modal.createDialog(InfoDialog, {
|
||||
|
@ -1028,7 +1030,7 @@ export const Commands = [
|
|||
const fingerprint = matches[3];
|
||||
|
||||
return success(
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
const device = cli.getStoredDevice(userId, deviceId);
|
||||
if (!device) {
|
||||
throw newTranslatableError("Unknown (user, session) pair: (%(userId)s, %(deviceId)s)", {
|
||||
|
@ -1205,7 +1207,7 @@ export const Commands = [
|
|||
},
|
||||
runFn: (roomId) => {
|
||||
return success(
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
const room = await VoipUserMapper.sharedInstance().getVirtualRoomForRoom(roomId);
|
||||
if (!room) throw newTranslatableError("No virtual room for this room");
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
|
@ -1231,7 +1233,7 @@ export const Commands = [
|
|||
}
|
||||
|
||||
return success(
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
if (isPhoneNumber) {
|
||||
const results = await LegacyCallHandler.instance.pstnLookup(userId);
|
||||
if (!results || results.length === 0 || !results[0].userid) {
|
||||
|
@ -1265,7 +1267,7 @@ export const Commands = [
|
|||
const [userId, msg] = matches.slice(1);
|
||||
if (userId && userId.startsWith("@") && userId.includes(":")) {
|
||||
return success(
|
||||
(async () => {
|
||||
(async (): Promise<void> => {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = await ensureDMExists(cli, userId);
|
||||
dis.dispatch<ViewRoomPayload>({
|
||||
|
|
|
@ -119,12 +119,10 @@ export class SlidingSyncManager {
|
|||
|
||||
public slidingSync: SlidingSync;
|
||||
private client: MatrixClient;
|
||||
private listIdToIndex: Record<string, number>;
|
||||
|
||||
private configureDefer: IDeferred<void>;
|
||||
|
||||
public constructor() {
|
||||
this.listIdToIndex = {};
|
||||
this.configureDefer = defer<void>();
|
||||
}
|
||||
|
||||
|
@ -134,13 +132,18 @@ export class SlidingSyncManager {
|
|||
|
||||
public configure(client: MatrixClient, proxyUrl: string): SlidingSync {
|
||||
this.client = client;
|
||||
this.listIdToIndex = {};
|
||||
// by default use the encrypted subscription as that gets everything, which is a safer
|
||||
// default than potentially missing member events.
|
||||
this.slidingSync = new SlidingSync(proxyUrl, [], ENCRYPTED_SUBSCRIPTION, client, SLIDING_SYNC_TIMEOUT_MS);
|
||||
this.slidingSync = new SlidingSync(
|
||||
proxyUrl,
|
||||
new Map(),
|
||||
ENCRYPTED_SUBSCRIPTION,
|
||||
client,
|
||||
SLIDING_SYNC_TIMEOUT_MS,
|
||||
);
|
||||
this.slidingSync.addCustomSubscription(UNENCRYPTED_SUBSCRIPTION_NAME, UNENCRYPTED_SUBSCRIPTION);
|
||||
// set the space list
|
||||
this.slidingSync.setList(this.getOrAllocateListIndex(SlidingSyncManager.ListSpaces), {
|
||||
this.slidingSync.setList(SlidingSyncManager.ListSpaces, {
|
||||
ranges: [[0, 20]],
|
||||
sort: ["by_name"],
|
||||
slow_get_all_rooms: true,
|
||||
|
@ -173,47 +176,16 @@ export class SlidingSyncManager {
|
|||
return this.slidingSync;
|
||||
}
|
||||
|
||||
public listIdForIndex(index: number): string | null {
|
||||
for (const listId in this.listIdToIndex) {
|
||||
if (this.listIdToIndex[listId] === index) {
|
||||
return listId;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allocate or retrieve the list index for an arbitrary list ID. For example SlidingSyncManager.ListSpaces
|
||||
* @param listId A string which represents the list.
|
||||
* @returns The index to use when registering lists or listening for callbacks.
|
||||
*/
|
||||
public getOrAllocateListIndex(listId: string): number {
|
||||
let index = this.listIdToIndex[listId];
|
||||
if (index === undefined) {
|
||||
// assign next highest index
|
||||
index = -1;
|
||||
for (const id in this.listIdToIndex) {
|
||||
const listIndex = this.listIdToIndex[id];
|
||||
if (listIndex > index) {
|
||||
index = listIndex;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
this.listIdToIndex[listId] = index;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that this list is registered.
|
||||
* @param listIndex The list index to register
|
||||
* @param listKey The list key to register
|
||||
* @param updateArgs The fields to update on the list.
|
||||
* @returns The complete list request params
|
||||
*/
|
||||
public async ensureListRegistered(listIndex: number, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
|
||||
logger.debug("ensureListRegistered:::", listIndex, updateArgs);
|
||||
public async ensureListRegistered(listKey: string, updateArgs: PartialSlidingSyncRequest): Promise<MSC3575List> {
|
||||
logger.debug("ensureListRegistered:::", listKey, updateArgs);
|
||||
await this.configureDefer.promise;
|
||||
let list = this.slidingSync.getList(listIndex);
|
||||
let list = this.slidingSync.getListParams(listKey);
|
||||
if (!list) {
|
||||
list = {
|
||||
ranges: [[0, 20]],
|
||||
|
@ -252,14 +224,14 @@ export class SlidingSyncManager {
|
|||
try {
|
||||
// if we only have range changes then call a different function so we don't nuke the list from before
|
||||
if (updateArgs.ranges && Object.keys(updateArgs).length === 1) {
|
||||
await this.slidingSync.setListRanges(listIndex, updateArgs.ranges);
|
||||
await this.slidingSync.setListRanges(listKey, updateArgs.ranges);
|
||||
} else {
|
||||
await this.slidingSync.setList(listIndex, list);
|
||||
await this.slidingSync.setList(listKey, list);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.debug("ensureListRegistered: update failed txn_id=", err);
|
||||
}
|
||||
return this.slidingSync.getList(listIndex);
|
||||
return this.slidingSync.getListParams(listKey)!;
|
||||
}
|
||||
|
||||
public async setRoomVisible(roomId: string, visible: boolean): Promise<string> {
|
||||
|
@ -302,9 +274,8 @@ export class SlidingSyncManager {
|
|||
* @param batchSize The number of rooms to return in each request.
|
||||
* @param gapBetweenRequestsMs The number of milliseconds to wait between requests.
|
||||
*/
|
||||
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number) {
|
||||
public async startSpidering(batchSize: number, gapBetweenRequestsMs: number): Promise<void> {
|
||||
await sleep(gapBetweenRequestsMs); // wait a bit as this is called on first render so let's let things load
|
||||
const listIndex = this.getOrAllocateListIndex(SlidingSyncManager.ListSearch);
|
||||
let startIndex = batchSize;
|
||||
let hasMore = true;
|
||||
let firstTime = true;
|
||||
|
@ -316,7 +287,7 @@ export class SlidingSyncManager {
|
|||
[startIndex, endIndex],
|
||||
];
|
||||
if (firstTime) {
|
||||
await this.slidingSync.setList(listIndex, {
|
||||
await this.slidingSync.setList(SlidingSyncManager.ListSearch, {
|
||||
// e.g [0,19] [20,39] then [0,19] [40,59]. We keep [0,20] constantly to ensure
|
||||
// any changes to the list whilst spidering are caught.
|
||||
ranges: ranges,
|
||||
|
@ -342,15 +313,17 @@ export class SlidingSyncManager {
|
|||
},
|
||||
});
|
||||
} else {
|
||||
await this.slidingSync.setListRanges(listIndex, ranges);
|
||||
await this.slidingSync.setListRanges(SlidingSyncManager.ListSearch, ranges);
|
||||
}
|
||||
// gradually request more over time
|
||||
await sleep(gapBetweenRequestsMs);
|
||||
} catch (err) {
|
||||
// do nothing, as we reject only when we get interrupted but that's fine as the next
|
||||
// request will include our data
|
||||
} finally {
|
||||
// gradually request more over time, even on errors.
|
||||
await sleep(gapBetweenRequestsMs);
|
||||
}
|
||||
hasMore = endIndex + 1 < this.slidingSync.getListData(listIndex)?.joinedCount;
|
||||
const listData = this.slidingSync.getListData(SlidingSyncManager.ListSearch)!;
|
||||
hasMore = endIndex + 1 < listData.joinedCount;
|
||||
startIndex += batchSize;
|
||||
firstTime = false;
|
||||
}
|
||||
|
|
|
@ -75,7 +75,7 @@ export type TermsInteractionCallback = (
|
|||
export async function startTermsFlow(
|
||||
services: Service[],
|
||||
interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
|
||||
) {
|
||||
): Promise<void> {
|
||||
const termsPromises = services.map((s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl));
|
||||
|
||||
/*
|
||||
|
@ -176,7 +176,7 @@ export async function startTermsFlow(
|
|||
urlsForService,
|
||||
);
|
||||
});
|
||||
return Promise.all(agreePromises);
|
||||
await Promise.all(agreePromises);
|
||||
}
|
||||
|
||||
export async function dialogTermsInteractionCallback(
|
||||
|
|
|
@ -20,7 +20,8 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
||||
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||
import { M_POLL_START, M_POLL_END, PollStartEvent } from "matrix-events-sdk";
|
||||
import { M_POLL_START, M_POLL_END } from "matrix-js-sdk/src/@types/polls";
|
||||
import { PollStartEvent } from "matrix-js-sdk/src/extensible_events_v1/PollStartEvent";
|
||||
|
||||
import { _t } from "./languageHandler";
|
||||
import * as Roles from "./Roles";
|
||||
|
@ -228,7 +229,7 @@ function textForTombstoneEvent(ev: MatrixEvent): () => string | null {
|
|||
return () => _t("%(senderDisplayName)s upgraded this room.", { senderDisplayName });
|
||||
}
|
||||
|
||||
const onViewJoinRuleSettingsClick = () => {
|
||||
const onViewJoinRuleSettingsClick = (): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: "open_room_settings",
|
||||
initial_tab_id: ROOM_SECURITY_TAB,
|
||||
|
|
|
@ -50,7 +50,7 @@ export default class UserActivity {
|
|||
this.activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS);
|
||||
}
|
||||
|
||||
public static sharedInstance() {
|
||||
public static sharedInstance(): UserActivity {
|
||||
if (window.mxUserActivity === undefined) {
|
||||
window.mxUserActivity = new UserActivity(window, document);
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ export default class UserActivity {
|
|||
* later on when the user does become active.
|
||||
* @param {Timer} timer the timer to use
|
||||
*/
|
||||
public timeWhileActiveNow(timer: Timer) {
|
||||
public timeWhileActiveNow(timer: Timer): void {
|
||||
this.timeWhile(timer, this.attachedActiveNowTimers);
|
||||
if (this.userActiveNow()) {
|
||||
timer.start();
|
||||
|
@ -82,14 +82,14 @@ export default class UserActivity {
|
|||
* later on when the user does become active.
|
||||
* @param {Timer} timer the timer to use
|
||||
*/
|
||||
public timeWhileActiveRecently(timer: Timer) {
|
||||
public timeWhileActiveRecently(timer: Timer): void {
|
||||
this.timeWhile(timer, this.attachedActiveRecentlyTimers);
|
||||
if (this.userActiveRecently()) {
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
private timeWhile(timer: Timer, attachedTimers: Timer[]) {
|
||||
private timeWhile(timer: Timer, attachedTimers: Timer[]): void {
|
||||
// important this happens first
|
||||
const index = attachedTimers.indexOf(timer);
|
||||
if (index === -1) {
|
||||
|
@ -113,7 +113,7 @@ export default class UserActivity {
|
|||
/**
|
||||
* Start listening to user activity
|
||||
*/
|
||||
public start() {
|
||||
public start(): void {
|
||||
this.document.addEventListener("mousedown", this.onUserActivity);
|
||||
this.document.addEventListener("mousemove", this.onUserActivity);
|
||||
this.document.addEventListener("keydown", this.onUserActivity);
|
||||
|
@ -133,7 +133,7 @@ export default class UserActivity {
|
|||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
public stop() {
|
||||
public stop(): void {
|
||||
this.document.removeEventListener("mousedown", this.onUserActivity);
|
||||
this.document.removeEventListener("mousemove", this.onUserActivity);
|
||||
this.document.removeEventListener("keydown", this.onUserActivity);
|
||||
|
@ -152,7 +152,7 @@ export default class UserActivity {
|
|||
* user's attention at any given moment.
|
||||
* @returns {boolean} true if user is currently 'active'
|
||||
*/
|
||||
public userActiveNow() {
|
||||
public userActiveNow(): boolean {
|
||||
return this.activeNowTimeout.isRunning();
|
||||
}
|
||||
|
||||
|
@ -164,11 +164,11 @@ export default class UserActivity {
|
|||
* (or they may have gone to make tea and left the window focused).
|
||||
* @returns {boolean} true if user has been active recently
|
||||
*/
|
||||
public userActiveRecently() {
|
||||
public userActiveRecently(): boolean {
|
||||
return this.activeRecentlyTimeout.isRunning();
|
||||
}
|
||||
|
||||
private onPageVisibilityChanged = (e) => {
|
||||
private onPageVisibilityChanged = (e): void => {
|
||||
if (this.document.visibilityState === "hidden") {
|
||||
this.activeNowTimeout.abort();
|
||||
this.activeRecentlyTimeout.abort();
|
||||
|
@ -177,12 +177,12 @@ export default class UserActivity {
|
|||
}
|
||||
};
|
||||
|
||||
private onWindowBlurred = () => {
|
||||
private onWindowBlurred = (): void => {
|
||||
this.activeNowTimeout.abort();
|
||||
this.activeRecentlyTimeout.abort();
|
||||
};
|
||||
|
||||
private onUserActivity = (event: MouseEvent) => {
|
||||
private onUserActivity = (event: MouseEvent): void => {
|
||||
// ignore anything if the window isn't focused
|
||||
if (!this.document.hasFocus()) return;
|
||||
|
||||
|
@ -214,7 +214,7 @@ export default class UserActivity {
|
|||
}
|
||||
};
|
||||
|
||||
private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) {
|
||||
private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer): Promise<void> {
|
||||
attachedTimers.forEach((t) => t.start());
|
||||
try {
|
||||
await timeout.finished();
|
||||
|
|
|
@ -87,7 +87,7 @@ interface IAction {
|
|||
};
|
||||
}
|
||||
|
||||
export const reducer = (state: IState, action: IAction) => {
|
||||
export const reducer: Reducer<IState, IAction> = (state: IState, action: IAction) => {
|
||||
switch (action.type) {
|
||||
case Type.Register: {
|
||||
if (!state.activeRef) {
|
||||
|
|
|
@ -26,7 +26,7 @@ interface IProps extends Omit<React.HTMLProps<HTMLDivElement>, "onKeyDown"> {}
|
|||
// https://www.w3.org/TR/wai-aria-practices-1.1/#toolbar
|
||||
// All buttons passed in children must use RovingTabIndex to set `onFocus`, `isActive`, `ref`
|
||||
const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
|
||||
const onKeyDown = (ev: React.KeyboardEvent) => {
|
||||
const onKeyDown = (ev: React.KeyboardEvent): void => {
|
||||
const target = ev.target as HTMLElement;
|
||||
// Don't interfere with input default keydown behaviour
|
||||
if (target.tagName === "INPUT") return;
|
||||
|
|
|
@ -33,7 +33,7 @@ interface IProps extends React.ComponentProps<typeof StyledCheckbox> {
|
|||
export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onChange, onClose, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
const onKeyDown = (e: React.KeyboardEvent): void => {
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
|
||||
|
@ -55,7 +55,7 @@ export const StyledMenuItemCheckbox: React.FC<IProps> = ({ children, label, onCh
|
|||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||
const onKeyUp = (e: React.KeyboardEvent): void => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Space:
|
||||
|
|
|
@ -33,7 +33,7 @@ interface IProps extends React.ComponentProps<typeof StyledRadioButton> {
|
|||
export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChange, onClose, ...props }) => {
|
||||
const [onFocus, isActive, ref] = useRovingTabIndex<HTMLInputElement>();
|
||||
|
||||
const onKeyDown = (e: React.KeyboardEvent) => {
|
||||
const onKeyDown = (e: React.KeyboardEvent): void => {
|
||||
let handled = true;
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
|
||||
|
@ -55,7 +55,7 @@ export const StyledMenuItemRadio: React.FC<IProps> = ({ children, label, onChang
|
|||
e.preventDefault();
|
||||
}
|
||||
};
|
||||
const onKeyUp = (e: React.KeyboardEvent) => {
|
||||
const onKeyUp = (e: React.KeyboardEvent): void => {
|
||||
const action = getKeyBindingsManager().getAccessibilityAction(e);
|
||||
switch (action) {
|
||||
case KeyBindingAction.Enter:
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||
import { AsyncActionFn, AsyncActionPayload } from "../dispatcher/payloads";
|
||||
|
||||
/**
|
||||
* Create an action thunk that will dispatch actions indicating the current
|
||||
|
@ -45,7 +45,7 @@ import { AsyncActionPayload } from "../dispatcher/payloads";
|
|||
* `fn`.
|
||||
*/
|
||||
export function asyncAction(id: string, fn: () => Promise<any>, pendingFn: () => any | null): AsyncActionPayload {
|
||||
const helper = (dispatch) => {
|
||||
const helper: AsyncActionFn = (dispatch) => {
|
||||
dispatch({
|
||||
action: id + ".pending",
|
||||
request: typeof pendingFn === "function" ? pendingFn() : undefined,
|
||||
|
|
|
@ -22,7 +22,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher";
|
|||
* Redirect to the correct device manager section
|
||||
* Based on the labs setting
|
||||
*/
|
||||
export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean) => {
|
||||
export const viewUserDeviceSettings = (isNewDeviceManagerEnabled: boolean): void => {
|
||||
defaultDispatcher.dispatch({
|
||||
action: Action.ViewUserSettings,
|
||||
initialTabId: isNewDeviceManagerEnabled ? UserTab.SessionManager : UserTab.Security,
|
||||
|
|
|
@ -56,7 +56,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
|||
};
|
||||
}
|
||||
|
||||
public updateCurrentRoom = async (room) => {
|
||||
public updateCurrentRoom = async (room): Promise<void> => {
|
||||
const eventIndex = EventIndexPeg.get();
|
||||
let stats;
|
||||
|
||||
|
@ -131,17 +131,17 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
|
|||
});
|
||||
}
|
||||
|
||||
private onDisable = async () => {
|
||||
private onDisable = async (): Promise<void> => {
|
||||
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
|
||||
Modal.createDialog(DisableEventIndexDialog, null, null, /* priority = */ false, /* static = */ true);
|
||||
};
|
||||
|
||||
private onCrawlerSleepTimeChange = (e) => {
|
||||
private onCrawlerSleepTimeChange = (e): void => {
|
||||
this.setState({ crawlerSleepTime: e.target.value });
|
||||
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
|
||||
};
|
||||
|
||||
public render() {
|
||||
public render(): JSX.Element {
|
||||
const brand = SdkConfig.get().brand;
|
||||
|
||||
let crawlerState;
|
||||
|
|
|
@ -125,7 +125,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent<IProps, I
|
|||
let info;
|
||||
try {
|
||||
if (secureSecretStorage) {
|
||||
await accessSecretStorage(async () => {
|
||||
await accessSecretStorage(async (): Promise<void> => {
|
||||
info = await MatrixClientPeg.get().prepareKeyBackupVersion(null /* random key */, {
|
||||
secureSecretStorage: true,
|
||||
});
|
||||
|
|
|
@ -350,7 +350,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
createSecretStorageKey: async () => this.recoveryKey,
|
||||
keyBackupInfo: this.state.backupInfo,
|
||||
setupNewKeyBackup: !this.state.backupInfo,
|
||||
getKeyBackupPassphrase: async () => {
|
||||
getKeyBackupPassphrase: async (): Promise<Uint8Array> => {
|
||||
// We may already have the backup key if we earlier went
|
||||
// through the restore backup path, so pass it along
|
||||
// rather than prompting again.
|
||||
|
@ -383,7 +383,9 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
private restoreBackup = async (): Promise<void> => {
|
||||
// It's possible we'll need the backup key later on for bootstrapping,
|
||||
// so let's stash it here, rather than prompting for it twice.
|
||||
const keyCallback = (k) => (this.backupKey = k);
|
||||
const keyCallback = (k: Uint8Array): void => {
|
||||
this.backupKey = k;
|
||||
};
|
||||
|
||||
const { finished } = Modal.createDialog(
|
||||
RestoreKeyBackupDialog,
|
||||
|
@ -420,7 +422,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
this.setState({ phase: Phase.ChooseKeyPassphrase });
|
||||
};
|
||||
|
||||
private onPassPhraseNextClick = async (e: React.FormEvent) => {
|
||||
private onPassPhraseNextClick = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
if (!this.passphraseField.current) return; // unmounting
|
||||
|
||||
|
@ -434,7 +436,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent<IProp
|
|||
this.setState({ phase: Phase.PassphraseConfirm });
|
||||
};
|
||||
|
||||
private onPassPhraseConfirmNextClick = async (e: React.FormEvent) => {
|
||||
private onPassPhraseConfirmNextClick = async (e: React.FormEvent): Promise<void> => {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||
|
|
|
@ -121,7 +121,7 @@ export default class ExportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
return false;
|
||||
};
|
||||
|
||||
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>, phrase: AnyPassphrase) => {
|
||||
private onPassphraseChange = (ev: React.ChangeEvent<HTMLInputElement>, phrase: AnyPassphrase): void => {
|
||||
this.setState({
|
||||
[phrase]: ev.target.value,
|
||||
} as Pick<IState, AnyPassphrase>);
|
||||
|
|
|
@ -91,7 +91,7 @@ export default class ImportE2eKeysDialog extends React.Component<IProps, IState>
|
|||
return false;
|
||||
};
|
||||
|
||||
private startImport(file: File, passphrase: string) {
|
||||
private startImport(file: File, passphrase: string): Promise<void> {
|
||||
this.setState({
|
||||
errStr: null,
|
||||
phase: Phase.Importing,
|
||||
|
|
|
@ -30,7 +30,7 @@ export class ManagedPlayback extends Playback {
|
|||
return super.play();
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
this.manager.destroyPlaybackInstance(this);
|
||||
super.destroy();
|
||||
}
|
||||
|
|
|
@ -145,7 +145,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
return true; // we don't ever care if the event had listeners, so just return "yes"
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
// Dev note: It's critical that we call stop() during cleanup to ensure that downstream callers
|
||||
// are aware of the final clock position before the user triggered an unload.
|
||||
// noinspection JSIgnoredPromiseFromCall - not concerned about being called async here
|
||||
|
@ -159,7 +159,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
}
|
||||
}
|
||||
|
||||
public async prepare() {
|
||||
public async prepare(): Promise<void> {
|
||||
// don't attempt to decode the media again
|
||||
// AudioContext.decodeAudioData detaches the array buffer `this.buf`
|
||||
// meaning it cannot be re-read
|
||||
|
@ -190,7 +190,7 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
this.context.decodeAudioData(
|
||||
this.buf,
|
||||
(b) => resolve(b),
|
||||
async (e) => {
|
||||
async (e): Promise<void> => {
|
||||
try {
|
||||
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
|
||||
// very well.
|
||||
|
@ -232,12 +232,12 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
|
||||
}
|
||||
|
||||
private onPlaybackEnd = async () => {
|
||||
private onPlaybackEnd = async (): Promise<void> => {
|
||||
await this.context.suspend();
|
||||
this.emit(PlaybackState.Stopped);
|
||||
};
|
||||
|
||||
public async play() {
|
||||
public async play(): Promise<void> {
|
||||
// We can't restart a buffer source, so we need to create a new one if we hit the end
|
||||
if (this.state === PlaybackState.Stopped) {
|
||||
this.disconnectSource();
|
||||
|
@ -256,13 +256,13 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
this.emit(PlaybackState.Playing);
|
||||
}
|
||||
|
||||
private disconnectSource() {
|
||||
private disconnectSource(): void {
|
||||
if (this.element) return; // leave connected, we can (and must) re-use it
|
||||
this.source?.disconnect();
|
||||
this.source?.removeEventListener("ended", this.onPlaybackEnd);
|
||||
}
|
||||
|
||||
private makeNewSourceBuffer() {
|
||||
private makeNewSourceBuffer(): void {
|
||||
if (this.element && this.source) return; // leave connected, we can (and must) re-use it
|
||||
|
||||
if (this.element) {
|
||||
|
@ -276,22 +276,22 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
|||
this.source.connect(this.context.destination);
|
||||
}
|
||||
|
||||
public async pause() {
|
||||
public async pause(): Promise<void> {
|
||||
await this.context.suspend();
|
||||
this.emit(PlaybackState.Paused);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
public async stop(): Promise<void> {
|
||||
await this.onPlaybackEnd();
|
||||
this.clock.flagStop();
|
||||
}
|
||||
|
||||
public async toggle() {
|
||||
public async toggle(): Promise<void> {
|
||||
if (this.isPlaying) await this.pause();
|
||||
else await this.play();
|
||||
}
|
||||
|
||||
public async skipTo(timeSeconds: number) {
|
||||
public async skipTo(timeSeconds: number): Promise<void> {
|
||||
// Dev note: this function talks a lot about clock desyncs. There is a clock running
|
||||
// independently to the audio context and buffer so that accurate human-perceptible
|
||||
// time can be exposed. The PlaybackClock class has more information, but the short
|
||||
|
|
|
@ -89,7 +89,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
return this.observable;
|
||||
}
|
||||
|
||||
private checkTime = (force = false) => {
|
||||
private checkTime = (force = false): void => {
|
||||
const now = this.timeSeconds; // calculated dynamically
|
||||
if (this.lastCheck !== now || force) {
|
||||
this.observable.update([now, this.durationSeconds]);
|
||||
|
@ -102,7 +102,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
* The placeholders will be overridden once known.
|
||||
* @param {MatrixEvent} event The event to use for placeholders.
|
||||
*/
|
||||
public populatePlaceholdersFrom(event: MatrixEvent) {
|
||||
public populatePlaceholdersFrom(event: MatrixEvent): void {
|
||||
const durationMs = Number(event.getContent()["info"]?.["duration"]);
|
||||
if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
|
||||
}
|
||||
|
@ -112,11 +112,11 @@ export class PlaybackClock implements IDestroyable {
|
|||
* This is to ensure the clock isn't skewed into thinking it is ~0.5s into
|
||||
* a clip when the duration is set.
|
||||
*/
|
||||
public flagLoadTime() {
|
||||
public flagLoadTime(): void {
|
||||
this.clipStart = this.context.currentTime;
|
||||
}
|
||||
|
||||
public flagStart() {
|
||||
public flagStart(): void {
|
||||
if (this.stopped) {
|
||||
this.clipStart = this.context.currentTime;
|
||||
this.stopped = false;
|
||||
|
@ -128,7 +128,7 @@ export class PlaybackClock implements IDestroyable {
|
|||
}
|
||||
}
|
||||
|
||||
public flagStop() {
|
||||
public flagStop(): void {
|
||||
this.stopped = true;
|
||||
|
||||
// Reset the clock time now so that the update going out will trigger components
|
||||
|
@ -136,13 +136,13 @@ export class PlaybackClock implements IDestroyable {
|
|||
this.clipStart = this.context.currentTime;
|
||||
}
|
||||
|
||||
public syncTo(contextTime: number, clipTime: number) {
|
||||
public syncTo(contextTime: number, clipTime: number): void {
|
||||
this.clipStart = contextTime - clipTime;
|
||||
this.stopped = false; // count as a mid-stream pause (if we were stopped)
|
||||
this.checkTime(true);
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
this.observable.close();
|
||||
if (this.timerId) clearInterval(this.timerId);
|
||||
}
|
||||
|
|
|
@ -38,13 +38,13 @@ export class PlaybackManager {
|
|||
* instances are paused.
|
||||
* @param playback Optional. The playback to leave untouched.
|
||||
*/
|
||||
public pauseAllExcept(playback?: Playback) {
|
||||
public pauseAllExcept(playback?: Playback): void {
|
||||
this.instances
|
||||
.filter((p) => p !== playback && p.currentState === PlaybackState.Playing)
|
||||
.forEach((p) => p.pause());
|
||||
}
|
||||
|
||||
public destroyPlaybackInstance(playback: ManagedPlayback) {
|
||||
public destroyPlaybackInstance(playback: ManagedPlayback): void {
|
||||
this.instances = this.instances.filter((p) => p !== playback);
|
||||
}
|
||||
|
||||
|
|
|
@ -75,28 +75,28 @@ export class PlaybackQueue {
|
|||
return queue;
|
||||
}
|
||||
|
||||
private persistClocks() {
|
||||
private persistClocks(): void {
|
||||
localStorage.setItem(
|
||||
`mx_voice_message_clocks_${this.room.roomId}`,
|
||||
JSON.stringify(Array.from(this.clockStates.entries())),
|
||||
);
|
||||
}
|
||||
|
||||
private loadClocks() {
|
||||
private loadClocks(): void {
|
||||
const val = localStorage.getItem(`mx_voice_message_clocks_${this.room.roomId}`);
|
||||
if (!!val) {
|
||||
this.clockStates = new Map<string, number>(JSON.parse(val));
|
||||
}
|
||||
}
|
||||
|
||||
public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback) {
|
||||
public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback): void {
|
||||
// We don't ever detach our listeners: we expect the Playback to clean up for us
|
||||
this.playbacks.set(mxEvent.getId(), playback);
|
||||
playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, mxEvent, state));
|
||||
playback.clockInfo.liveData.onUpdate((clock) => this.onPlaybackClock(playback, mxEvent, clock));
|
||||
}
|
||||
|
||||
private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState) {
|
||||
private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState): void {
|
||||
// Remember where the user got to in playback
|
||||
const wasLastPlaying = this.currentPlaybackId === mxEvent.getId();
|
||||
if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()) && !wasLastPlaying) {
|
||||
|
@ -210,7 +210,7 @@ export class PlaybackQueue {
|
|||
}
|
||||
}
|
||||
|
||||
private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]) {
|
||||
private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]): void {
|
||||
if (playback.currentState === PlaybackState.Decoding) return; // ignore pre-ready values
|
||||
|
||||
if (playback.currentState !== PlaybackState.Stopped) {
|
||||
|
|
|
@ -43,7 +43,7 @@ class MxVoiceWorklet extends AudioWorkletProcessor {
|
|||
private nextAmplitudeSecond = 0;
|
||||
private amplitudeIndex = 0;
|
||||
|
||||
public process(inputs, outputs, parameters) {
|
||||
public process(inputs, outputs, parameters): boolean {
|
||||
const currentSecond = roundTimeToTargetFreq(currentTime);
|
||||
// We special case the first ping because there's a fairly good chance that we'll miss the zeroth
|
||||
// update. Firefox for instance takes 0.06 seconds (roughly) to call this function for the first
|
||||
|
|
|
@ -141,7 +141,7 @@ export class VoiceMessageRecording implements IDestroyable {
|
|||
this.voiceRecording.destroy();
|
||||
}
|
||||
|
||||
private onDataAvailable = (data: ArrayBuffer) => {
|
||||
private onDataAvailable = (data: ArrayBuffer): void => {
|
||||
const buf = new Uint8Array(data);
|
||||
this.buffer = concat(this.buffer, buf);
|
||||
};
|
||||
|
@ -153,6 +153,6 @@ export class VoiceMessageRecording implements IDestroyable {
|
|||
}
|
||||
}
|
||||
|
||||
export const createVoiceMessageRecording = (matrixClient: MatrixClient) => {
|
||||
export const createVoiceMessageRecording = (matrixClient: MatrixClient): VoiceMessageRecording => {
|
||||
return new VoiceMessageRecording(matrixClient, new VoiceRecording());
|
||||
};
|
||||
|
|
|
@ -110,7 +110,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
return !MediaDeviceHandler.getAudioNoiseSuppression();
|
||||
}
|
||||
|
||||
private async makeRecorder() {
|
||||
private async makeRecorder(): Promise<void> {
|
||||
try {
|
||||
this.recorderStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
|
@ -212,14 +212,14 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
return !!Recorder.isRecordingSupported();
|
||||
}
|
||||
|
||||
private onAudioProcess = (ev: AudioProcessingEvent) => {
|
||||
private onAudioProcess = (ev: AudioProcessingEvent): void => {
|
||||
this.processAudioUpdate(ev.playbackTime);
|
||||
|
||||
// We skip the functionality of the worklet regarding waveform calculations: we
|
||||
// should get that information pretty quick during the playback info.
|
||||
};
|
||||
|
||||
private processAudioUpdate = (timeSeconds: number) => {
|
||||
private processAudioUpdate = (timeSeconds: number): void => {
|
||||
if (!this.recording) return;
|
||||
|
||||
this.observable.update({
|
||||
|
@ -260,7 +260,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
/**
|
||||
* {@link https://github.com/chris-rudmin/opus-recorder#instance-fields ref for recorderSeconds}
|
||||
*/
|
||||
public get recorderSeconds() {
|
||||
public get recorderSeconds(): number {
|
||||
return this.recorder.encodedSamplePosition / 48000;
|
||||
}
|
||||
|
||||
|
@ -279,7 +279,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
return Singleflight.for(this, "stop").do(async () => {
|
||||
return Singleflight.for(this, "stop").do(async (): Promise<void> => {
|
||||
if (!this.recording) {
|
||||
throw new Error("No recording to stop");
|
||||
}
|
||||
|
@ -307,7 +307,7 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
|
|||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
// noinspection JSIgnoredPromiseFromCall - not concerned about stop() being called async here
|
||||
this.stop();
|
||||
this.removeAllListeners();
|
||||
|
|
|
@ -22,7 +22,7 @@ import { TimelineRenderingType } from "../contexts/RoomContext";
|
|||
import type { ICompletion, ISelectionRange } from "./Autocompleter";
|
||||
|
||||
export interface ICommand {
|
||||
command: string | null;
|
||||
command: RegExpExecArray | null;
|
||||
range: {
|
||||
start: number;
|
||||
end: number;
|
||||
|
@ -59,7 +59,7 @@ export default abstract class AutocompleteProvider {
|
|||
}
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
// stub
|
||||
}
|
||||
|
||||
|
@ -70,7 +70,7 @@ export default abstract class AutocompleteProvider {
|
|||
* @param {boolean} force True if the user is forcing completion
|
||||
* @return {object} { command, range } where both objects fields are null if no match
|
||||
*/
|
||||
public getCurrentCommand(query: string, selection: ISelectionRange, force = false) {
|
||||
public getCurrentCommand(query: string, selection: ISelectionRange, force = false): ICommand {
|
||||
let commandRegex = this.commandRegex;
|
||||
|
||||
if (force && this.shouldForceComplete()) {
|
||||
|
@ -83,7 +83,7 @@ export default abstract class AutocompleteProvider {
|
|||
|
||||
commandRegex.lastIndex = 0;
|
||||
|
||||
let match;
|
||||
let match: RegExpExecArray;
|
||||
while ((match = commandRegex.exec(query)) !== null) {
|
||||
const start = match.index;
|
||||
const end = start + match[0].length;
|
||||
|
|
|
@ -69,7 +69,7 @@ export default class Autocompleter {
|
|||
});
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
public destroy(): void {
|
||||
this.providers.forEach((p) => {
|
||||
p.destroy();
|
||||
});
|
||||
|
@ -88,7 +88,7 @@ export default class Autocompleter {
|
|||
*/
|
||||
// list of results from each provider, each being a list of completions or null if it times out
|
||||
const completionsList: ICompletion[][] = await Promise.all(
|
||||
this.providers.map(async (provider) => {
|
||||
this.providers.map(async (provider): Promise<ICompletion[] | null> => {
|
||||
return timeout(
|
||||
provider.getCompletions(query, selection, force, limit),
|
||||
null,
|
||||
|
|
|
@ -100,7 +100,7 @@ export default class CommandProvider extends AutocompleteProvider {
|
|||
});
|
||||
}
|
||||
|
||||
public getName() {
|
||||
public getName(): string {
|
||||
return "*️⃣ " + _t("Commands");
|
||||
}
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
|
|||
_orderBy: index,
|
||||
}));
|
||||
|
||||
function score(query, space) {
|
||||
function score(query: string, space: string): number {
|
||||
const index = space.indexOf(query);
|
||||
if (index === -1) {
|
||||
return Infinity;
|
||||
|
@ -154,7 +154,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
|||
return [];
|
||||
}
|
||||
|
||||
public getName() {
|
||||
public getName(): string {
|
||||
return "😃 " + _t("Emoji");
|
||||
}
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ export default class NotifProvider extends AutocompleteProvider {
|
|||
return [];
|
||||
}
|
||||
|
||||
public getName() {
|
||||
public getName(): string {
|
||||
return "❗️ " + _t("Room Notification");
|
||||
}
|
||||
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue