Merge branch 'develop' into staging

This commit is contained in:
RiotRobot 2023-01-24 11:08:34 +00:00
commit 7919c69bf7
803 changed files with 9375 additions and 6928 deletions

View file

@ -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",

View file

@ -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

View file

@ -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/*

View file

@ -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: {

View file

@ -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");

View file

@ -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");

View file

@ -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);

View file

@ -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;
});
});
});
});

View file

@ -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(

View file

@ -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],
},
});
});
});
};

View file

@ -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"));

View 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",
);
});
});
});
});
});
});

View file

@ -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",

View file

@ -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,
});
},
});
}

View file

@ -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,
});
},
});
});

View file

@ -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,
}));
}

View file

@ -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

View file

@ -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"
}
}

View file

@ -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";

View file

@ -16,6 +16,7 @@ limitations under the License.
/* 1rem :: 10px */
$spacing-2: 2px;
$spacing-4: 4px;
$spacing-8: 8px;
$spacing-12: 12px;

View file

@ -84,7 +84,7 @@ limitations under the License.
align-items: center;
&:hover,
&:focus {
&:focus-visible {
background-color: $menu-selected-color;
}

View file

@ -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");
}

View file

@ -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));
}

View file

@ -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;

View file

@ -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;

View file

@ -108,7 +108,7 @@ limitations under the License.
display: flex;
user-select: none;
&:not(.mx_RoomHeader_name--textonly):hover {
&:hover {
background-color: $quinary-content;
}

View file

@ -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 {

View file

@ -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);

View file

@ -17,4 +17,5 @@ limitations under the License.
.mx_CallDuration {
color: $secondary-content;
font-size: $font-12px;
white-space: nowrap;
}

View file

@ -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 {

View file

@ -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;
}
}

View 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

View 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

View 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

View 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

View 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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 {}

View file

@ -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) {

View file

@ -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} />;

View file

@ -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()) {

View file

@ -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;

View file

@ -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) {

View file

@ -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();
}

View file

@ -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());
}

View file

@ -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());

View file

@ -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}`;

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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,
{

View file

@ -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();

View file

@ -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, {

View file

@ -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);

View file

@ -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 = "";

View file

@ -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);

View file

@ -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

View file

@ -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();

View file

@ -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,

View file

@ -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;
}

View file

@ -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
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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 });
}
}

View file

@ -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,

View file

@ -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;

View file

@ -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>({

View file

@ -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;
}

View file

@ -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(

View file

@ -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,

View file

@ -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();

View file

@ -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) {

View file

@ -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;

View file

@ -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:

View file

@ -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:

View file

@ -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,

View file

@ -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,

View file

@ -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;

View file

@ -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,
});

View file

@ -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;

View file

@ -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>);

View file

@ -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,

View file

@ -30,7 +30,7 @@ export class ManagedPlayback extends Playback {
return super.play();
}
public destroy() {
public destroy(): void {
this.manager.destroyPlaybackInstance(this);
super.destroy();
}

View file

@ -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

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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) {

View file

@ -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

View file

@ -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());
};

View file

@ -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();

View file

@ -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;

View file

@ -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,

View file

@ -100,7 +100,7 @@ export default class CommandProvider extends AutocompleteProvider {
});
}
public getName() {
public getName(): string {
return "*️⃣ " + _t("Commands");
}

View file

@ -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");
}

View file

@ -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