Merge branch 'develop' into gsouquet/threads-forceenablelabsflag
This commit is contained in:
commit
d4f247d1fe
97 changed files with 3280 additions and 1325 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -24,6 +24,7 @@ package-lock.json
|
||||||
/cypress/downloads
|
/cypress/downloads
|
||||||
/cypress/screenshots
|
/cypress/screenshots
|
||||||
/cypress/synapselogs
|
/cypress/synapselogs
|
||||||
|
/cypress/dendritelogs
|
||||||
# These could have files in them but don't currently
|
# These could have files in them but don't currently
|
||||||
# Cypress will still auto-create them though...
|
# Cypress will still auto-create them though...
|
||||||
/cypress/performance
|
/cypress/performance
|
||||||
|
|
|
@ -33,6 +33,7 @@ export default defineConfig({
|
||||||
env: {
|
env: {
|
||||||
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
// Docker tag to use for `ghcr.io/matrix-org/sliding-sync-proxy` image.
|
||||||
SLIDING_SYNC_PROXY_TAG: "v0.6.0",
|
SLIDING_SYNC_PROXY_TAG: "v0.6.0",
|
||||||
|
HOMESERVER: "synapse",
|
||||||
},
|
},
|
||||||
retries: {
|
retries: {
|
||||||
runMode: 4,
|
runMode: 4,
|
||||||
|
|
|
@ -16,25 +16,25 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
describe("Composer", () => {
|
describe("Composer", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("CIDER", () => {
|
describe("CIDER", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.initTestUser(synapse, "Janet").then(() => {
|
cy.initTestUser(homeserver, "Janet").then(() => {
|
||||||
cy.createRoom({ name: "Composing Room" });
|
cy.createRoom({ name: "Composing Room" });
|
||||||
});
|
});
|
||||||
cy.viewRoomByName("Composing Room");
|
cy.viewRoomByName("Composing Room");
|
||||||
|
@ -101,7 +101,7 @@ describe("Composer", () => {
|
||||||
describe("WYSIWYG", () => {
|
describe("WYSIWYG", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.enableLabsFeature("feature_wysiwyg_composer");
|
cy.enableLabsFeature("feature_wysiwyg_composer");
|
||||||
cy.initTestUser(synapse, "Janet").then(() => {
|
cy.initTestUser(homeserver, "Janet").then(() => {
|
||||||
cy.createRoom({ name: "Composing Room" });
|
cy.createRoom({ name: "Composing Room" });
|
||||||
});
|
});
|
||||||
cy.viewRoomByName("Composing Room");
|
cy.viewRoomByName("Composing Room");
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
function openCreateRoomDialog(): Chainable<JQuery<HTMLElement>> {
|
function openCreateRoomDialog(): Chainable<JQuery<HTMLElement>> {
|
||||||
|
@ -26,18 +26,18 @@ function openCreateRoomDialog(): Chainable<JQuery<HTMLElement>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Create Room", () => {
|
describe("Create Room", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Jim");
|
cy.initTestUser(homeserver, "Jim");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow us to create a public room with name, topic & address set", () => {
|
it("should allow us to create a public room with name, topic & address set", () => {
|
||||||
|
|
|
@ -18,12 +18,12 @@ import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/m
|
||||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
import type { CypressBot } from "../../support/bot";
|
import type { CypressBot } from "../../support/bot";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
type EmojiMapping = [emoji: string, name: string];
|
type EmojiMapping = [emoji: string, name: string];
|
||||||
interface CryptoTestContext extends Mocha.Context {
|
interface CryptoTestContext extends Mocha.Context {
|
||||||
synapse: SynapseInstance;
|
homeserver: HomeserverInstance;
|
||||||
bob: CypressBot;
|
bob: CypressBot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,16 +155,16 @@ const verify = function (this: CryptoTestContext) {
|
||||||
|
|
||||||
describe("Cryptography", function () {
|
describe("Cryptography", function () {
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
cy.startSynapse("default")
|
cy.startHomeserver("default")
|
||||||
.as("synapse")
|
.as("homeserver")
|
||||||
.then((synapse: SynapseInstance) => {
|
.then((homeserver: HomeserverInstance) => {
|
||||||
cy.initTestUser(synapse, "Alice", undefined, "alice_");
|
cy.initTestUser(homeserver, "Alice", undefined, "alice_");
|
||||||
cy.getBot(synapse, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
cy.getBot(homeserver, { displayName: "Bob", autoAcceptInvites: false, userIdPrefix: "bob_" }).as("bob");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(function (this: CryptoTestContext) {
|
afterEach(function (this: CryptoTestContext) {
|
||||||
cy.stopSynapse(this.synapse);
|
cy.stopHomeserver(this.homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("setting up secure key backup should work", () => {
|
it("setting up secure key backup should work", () => {
|
||||||
|
@ -215,7 +215,7 @@ describe("Cryptography", function () {
|
||||||
cy.bootstrapCrossSigning();
|
cy.bootstrapCrossSigning();
|
||||||
|
|
||||||
// bob has a second, not cross-signed, device
|
// bob has a second, not cross-signed, device
|
||||||
cy.loginBot(this.synapse, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
cy.loginBot(this.homeserver, this.bob.getUserId(), this.bob.__cypress_password, {}).as("bobSecondDevice");
|
||||||
|
|
||||||
autoJoin(this.bob);
|
autoJoin(this.bob);
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import type { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
import type { ISasEvent } from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import type { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
@ -56,20 +56,20 @@ const handleVerificationRequest = (request: VerificationRequest): Chainable<Emoj
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Decryption Failure Bar", () => {
|
describe("Decryption Failure Bar", () => {
|
||||||
let synapse: SynapseInstance | undefined;
|
let homeserver: HomeserverInstance | undefined;
|
||||||
let testUser: UserCredentials | undefined;
|
let testUser: UserCredentials | undefined;
|
||||||
let bot: MatrixClient | undefined;
|
let bot: MatrixClient | undefined;
|
||||||
let roomId: string;
|
let roomId: string;
|
||||||
|
|
||||||
beforeEach(function () {
|
beforeEach(function () {
|
||||||
cy.startSynapse("default").then((syn: SynapseInstance) => {
|
cy.startHomeserver("default").then((hs: HomeserverInstance) => {
|
||||||
synapse = syn;
|
homeserver = hs;
|
||||||
cy.initTestUser(synapse, TEST_USER)
|
cy.initTestUser(homeserver, TEST_USER)
|
||||||
.then((creds: UserCredentials) => {
|
.then((creds: UserCredentials) => {
|
||||||
testUser = creds;
|
testUser = creds;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
cy.getBot(synapse, { displayName: BOT_USER }).then((cli) => {
|
cy.getBot(homeserver, { displayName: BOT_USER }).then((cli) => {
|
||||||
bot = cli;
|
bot = cli;
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -97,7 +97,7 @@ describe("Decryption Failure Bar", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it(
|
||||||
|
@ -105,7 +105,7 @@ describe("Decryption Failure Bar", () => {
|
||||||
"and there are other verified devices or backups",
|
"and there are other verified devices or backups",
|
||||||
() => {
|
() => {
|
||||||
let otherDevice: MatrixClient | undefined;
|
let otherDevice: MatrixClient | undefined;
|
||||||
cy.loginBot(synapse, testUser.username, testUser.password, {})
|
cy.loginBot(homeserver, testUser.username, testUser.password, {})
|
||||||
.then(async (cli) => {
|
.then(async (cli) => {
|
||||||
otherDevice = cli;
|
otherDevice = cli;
|
||||||
await otherDevice.bootstrapCrossSigning({
|
await otherDevice.bootstrapCrossSigning({
|
||||||
|
@ -169,7 +169,7 @@ describe("Decryption Failure Bar", () => {
|
||||||
"should prompt the user to reset keys, if this device isn't verified " +
|
"should prompt the user to reset keys, if this device isn't verified " +
|
||||||
"and there are no other verified devices or backups",
|
"and there are no other verified devices or backups",
|
||||||
() => {
|
() => {
|
||||||
cy.loginBot(synapse, testUser.username, testUser.password, {}).then(async (cli) => {
|
cy.loginBot(homeserver, testUser.username, testUser.password, {}).then(async (cli) => {
|
||||||
await cli.bootstrapCrossSigning({
|
await cli.bootstrapCrossSigning({
|
||||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||||
await makeRequest({});
|
await makeRequest({});
|
||||||
|
|
|
@ -16,24 +16,26 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { MessageEvent } from "matrix-events-sdk";
|
import type { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||||
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
|
const sendEvent = (roomId: string): Chainable<ISendEventResponse> => {
|
||||||
return cy.sendEvent(roomId, null, "m.room.message" as EventType, MessageEvent.from("Message").serialize().content);
|
return cy.sendEvent(roomId, null, "m.room.message" as EventType, {
|
||||||
|
msgtype: "m.text" as MsgType,
|
||||||
|
body: "Message",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Editing", () => {
|
describe("Editing", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Edith").then(() => {
|
cy.initTestUser(homeserver, "Edith").then(() => {
|
||||||
cy.injectAxe();
|
cy.injectAxe();
|
||||||
return cy.createRoom({ name: "Test room" }).as("roomId");
|
return cy.createRoom({ name: "Test room" }).as("roomId");
|
||||||
});
|
});
|
||||||
|
@ -41,7 +43,7 @@ describe("Editing", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should close the composer when clicking save after making a change and undoing it", () => {
|
it("should close the composer when clicking save after making a change and undoing it", () => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
const ROOM_NAME = "Integration Manager Test";
|
const ROOM_NAME = "Integration Manager Test";
|
||||||
|
@ -73,17 +73,17 @@ function sendActionFromIntegrationManager(integrationManagerUrl: string) {
|
||||||
|
|
||||||
describe("Integration Manager: Get OpenID Token", () => {
|
describe("Integration Manager: Get OpenID Token", () => {
|
||||||
let testUser: UserCredentials;
|
let testUser: UserCredentials;
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let integrationManagerUrl: string;
|
let integrationManagerUrl: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||||
integrationManagerUrl = url;
|
integrationManagerUrl = url;
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||||
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||||
|
@ -122,7 +122,7 @@ describe("Integration Manager: Get OpenID Token", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
|
@ -94,17 +94,17 @@ function expectKickedMessage(shouldExist: boolean) {
|
||||||
|
|
||||||
describe("Integration Manager: Kick", () => {
|
describe("Integration Manager: Kick", () => {
|
||||||
let testUser: UserCredentials;
|
let testUser: UserCredentials;
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let integrationManagerUrl: string;
|
let integrationManagerUrl: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||||
integrationManagerUrl = url;
|
integrationManagerUrl = url;
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||||
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||||
|
@ -140,12 +140,12 @@ describe("Integration Manager: Kick", () => {
|
||||||
name: ROOM_NAME,
|
name: ROOM_NAME,
|
||||||
}).as("roomId");
|
}).as("roomId");
|
||||||
|
|
||||||
cy.getBot(synapse, { displayName: BOT_DISPLAY_NAME, autoAcceptInvites: true }).as("bob");
|
cy.getBot(homeserver, { displayName: BOT_DISPLAY_NAME, autoAcceptInvites: true }).as("bob");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
const ROOM_NAME = "Integration Manager Test";
|
const ROOM_NAME = "Integration Manager Test";
|
||||||
|
@ -87,17 +87,17 @@ function sendActionFromIntegrationManager(
|
||||||
|
|
||||||
describe("Integration Manager: Read Events", () => {
|
describe("Integration Manager: Read Events", () => {
|
||||||
let testUser: UserCredentials;
|
let testUser: UserCredentials;
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let integrationManagerUrl: string;
|
let integrationManagerUrl: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||||
integrationManagerUrl = url;
|
integrationManagerUrl = url;
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||||
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||||
|
@ -136,7 +136,7 @@ describe("Integration Manager: Read Events", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
const ROOM_NAME = "Integration Manager Test";
|
const ROOM_NAME = "Integration Manager Test";
|
||||||
|
@ -93,17 +93,17 @@ function sendActionFromIntegrationManager(
|
||||||
|
|
||||||
describe("Integration Manager: Send Event", () => {
|
describe("Integration Manager: Send Event", () => {
|
||||||
let testUser: UserCredentials;
|
let testUser: UserCredentials;
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let integrationManagerUrl: string;
|
let integrationManagerUrl: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
cy.serveHtmlFile(INTEGRATION_MANAGER_HTML).then((url) => {
|
||||||
integrationManagerUrl = url;
|
integrationManagerUrl = url;
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, USER_DISPLAY_NAME, () => {
|
cy.initTestUser(homeserver, USER_DISPLAY_NAME, () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem("mx_scalar_token", INTEGRATION_MANAGER_TOKEN);
|
||||||
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
win.localStorage.setItem(`mx_scalar_token_at_${integrationManagerUrl}`, INTEGRATION_MANAGER_TOKEN);
|
||||||
|
@ -142,7 +142,7 @@ describe("Integration Manager: Send Event", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ interface Charly {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Lazy Loading", () => {
|
describe("Lazy Loading", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let bob: MatrixClient;
|
let bob: MatrixClient;
|
||||||
const charlies: Charly[] = [];
|
const charlies: Charly[] = [];
|
||||||
|
|
||||||
|
@ -35,12 +35,12 @@ describe("Lazy Loading", () => {
|
||||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Alice");
|
cy.initTestUser(homeserver, "Alice");
|
||||||
|
|
||||||
cy.getBot(synapse, {
|
cy.getBot(homeserver, {
|
||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
startClient: false,
|
startClient: false,
|
||||||
autoAcceptInvites: false,
|
autoAcceptInvites: false,
|
||||||
|
@ -50,7 +50,7 @@ describe("Lazy Loading", () => {
|
||||||
|
|
||||||
for (let i = 1; i <= 10; i++) {
|
for (let i = 1; i <= 10; i++) {
|
||||||
const displayName = `Charly #${i}`;
|
const displayName = `Charly #${i}`;
|
||||||
cy.getBot(synapse, {
|
cy.getBot(homeserver, {
|
||||||
displayName,
|
displayName,
|
||||||
startClient: false,
|
startClient: false,
|
||||||
autoAcceptInvites: false,
|
autoAcceptInvites: false,
|
||||||
|
@ -62,7 +62,7 @@ describe("Lazy Loading", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
const name = "Lazy Loading Test";
|
const name = "Lazy Loading Test";
|
||||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
describe("Location sharing", () => {
|
describe("Location sharing", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
const selectLocationShareTypeOption = (shareType: string): Chainable<JQuery> => {
|
const selectLocationShareTypeOption = (shareType: string): Chainable<JQuery> => {
|
||||||
return cy.get(`[data-test-id="share-location-option-${shareType}"]`);
|
return cy.get(`[data-test-id="share-location-option-${shareType}"]`);
|
||||||
|
@ -34,15 +34,15 @@ describe("Location sharing", () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Tom");
|
cy.initTestUser(homeserver, "Tom");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends and displays pin drop location message successfully", () => {
|
it("sends and displays pin drop location message successfully", () => {
|
||||||
|
|
|
@ -18,21 +18,21 @@ limitations under the License.
|
||||||
|
|
||||||
import { SinonStub } from "cypress/types/sinon";
|
import { SinonStub } from "cypress/types/sinon";
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("Consent", () => {
|
describe("Consent", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("consent").then((data) => {
|
cy.startHomeserver("consent").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Bob");
|
cy.initTestUser(homeserver, "Bob");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should prompt the user to consent to terms when server deems it necessary", () => {
|
it("should prompt the user to consent to terms when server deems it necessary", () => {
|
||||||
|
@ -53,8 +53,8 @@ describe("Consent", () => {
|
||||||
cy.get<SinonStub>("@windowOpen").then((stub) => {
|
cy.get<SinonStub>("@windowOpen").then((stub) => {
|
||||||
const url = stub.getCall(0).args[0];
|
const url = stub.getCall(0).args[0];
|
||||||
|
|
||||||
// Go to Synapse's consent page and accept it
|
// Go to Homeserver's consent page and accept it
|
||||||
cy.origin(synapse.baseUrl, { args: { url } }, ({ url }) => {
|
cy.origin(homeserver.baseUrl, { args: { url } }, ({ url }) => {
|
||||||
cy.visit(url);
|
cy.visit(url);
|
||||||
|
|
||||||
cy.get('[type="submit"]').click();
|
cy.get('[type="submit"]').click();
|
||||||
|
|
|
@ -16,17 +16,17 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("Login", () => {
|
describe("Login", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.stubDefaultServer();
|
cy.stubDefaultServer();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("m.login.password", () => {
|
describe("m.login.password", () => {
|
||||||
|
@ -34,9 +34,9 @@ describe("Login", () => {
|
||||||
const password = "p4s5W0rD";
|
const password = "p4s5W0rD";
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("consent").then((data) => {
|
cy.startHomeserver("consent").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.registerUser(synapse, username, password);
|
cy.registerUser(homeserver, username, password);
|
||||||
cy.visit("/#/login");
|
cy.visit("/#/login");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,7 +49,7 @@ describe("Login", () => {
|
||||||
cy.checkA11y();
|
cy.checkA11y();
|
||||||
|
|
||||||
cy.get(".mx_ServerPicker_change").click();
|
cy.get(".mx_ServerPicker_change").click();
|
||||||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(homeserver.baseUrl);
|
||||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||||
// wait for the dialog to go away
|
// wait for the dialog to go away
|
||||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||||
|
@ -64,9 +64,9 @@ describe("Login", () => {
|
||||||
|
|
||||||
describe("logout", () => {
|
describe("logout", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("consent").then((data) => {
|
cy.startHomeserver("consent").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Erin");
|
cy.initTestUser(homeserver, "Erin");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -18,14 +18,14 @@ limitations under the License.
|
||||||
|
|
||||||
import { PollResponseEvent } from "matrix-events-sdk";
|
import { PollResponseEvent } from "matrix-events-sdk";
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
|
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
|
||||||
|
|
||||||
describe("Polls", () => {
|
describe("Polls", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
type CreatePollOptions = {
|
type CreatePollOptions = {
|
||||||
title: string;
|
title: string;
|
||||||
|
@ -81,20 +81,20 @@ describe("Polls", () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Tom");
|
cy.initTestUser(homeserver, "Tom");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be creatable and votable", () => {
|
it("should be creatable and votable", () => {
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
||||||
bot = _bot;
|
bot = _bot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ describe("Polls", () => {
|
||||||
|
|
||||||
it("should be editable from context menu if no votes have been cast", () => {
|
it("should be editable from context menu if no votes have been cast", () => {
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
||||||
bot = _bot;
|
bot = _bot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ describe("Polls", () => {
|
||||||
|
|
||||||
it("should not be editable from context menu if votes have been cast", () => {
|
it("should not be editable from context menu if votes have been cast", () => {
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
||||||
bot = _bot;
|
bot = _bot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -256,10 +256,10 @@ describe("Polls", () => {
|
||||||
it("should be displayed correctly in thread panel", () => {
|
it("should be displayed correctly in thread panel", () => {
|
||||||
let botBob: MatrixClient;
|
let botBob: MatrixClient;
|
||||||
let botCharlie: MatrixClient;
|
let botCharlie: MatrixClient;
|
||||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
||||||
botBob = _bot;
|
botBob = _bot;
|
||||||
});
|
});
|
||||||
cy.getBot(synapse, { displayName: "BotCharlie" }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "BotCharlie" }).then((_bot) => {
|
||||||
botCharlie = _bot;
|
botCharlie = _bot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,21 +16,21 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("Registration", () => {
|
describe("Registration", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.stubDefaultServer();
|
cy.stubDefaultServer();
|
||||||
cy.visit("/#/register");
|
cy.visit("/#/register");
|
||||||
cy.startSynapse("consent").then((data) => {
|
cy.startHomeserver("consent").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("registers an account and lands on the home screen", () => {
|
it("registers an account and lands on the home screen", () => {
|
||||||
|
@ -42,13 +42,13 @@ describe("Registration", () => {
|
||||||
cy.get(".mx_Dialog").percySnapshotElement("Server Picker", { widths: [516] });
|
cy.get(".mx_Dialog").percySnapshotElement("Server Picker", { widths: [516] });
|
||||||
cy.checkA11y();
|
cy.checkA11y();
|
||||||
|
|
||||||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(homeserver.baseUrl);
|
||||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||||
// wait for the dialog to go away
|
// wait for the dialog to go away
|
||||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||||
|
|
||||||
cy.get("#mx_RegistrationForm_username").should("be.visible");
|
cy.get("#mx_RegistrationForm_username").should("be.visible");
|
||||||
// Hide the server text as it contains the randomly allocated Synapse port
|
// Hide the server text as it contains the randomly allocated Homeserver port
|
||||||
const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }";
|
const percyCSS = ".mx_ServerPicker_server { visibility: hidden !important; }";
|
||||||
cy.percySnapshot("Registration", { percyCSS });
|
cy.percySnapshot("Registration", { percyCSS });
|
||||||
cy.checkA11y();
|
cy.checkA11y();
|
||||||
|
@ -88,7 +88,7 @@ describe("Registration", () => {
|
||||||
it("should require username to fulfil requirements and be available", () => {
|
it("should require username to fulfil requirements and be available", () => {
|
||||||
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
|
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
|
||||||
cy.get(".mx_ServerPickerDialog_continue").should("be.visible");
|
cy.get(".mx_ServerPickerDialog_continue").should("be.visible");
|
||||||
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(synapse.baseUrl);
|
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(homeserver.baseUrl);
|
||||||
cy.get(".mx_ServerPickerDialog_continue").click();
|
cy.get(".mx_ServerPickerDialog_continue").click();
|
||||||
// wait for the dialog to go away
|
// wait for the dialog to go away
|
||||||
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
cy.get(".mx_ServerPickerDialog").should("not.exist");
|
||||||
|
|
|
@ -16,21 +16,21 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("Pills", () => {
|
describe("Pills", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Sally");
|
cy.initTestUser(homeserver, "Sally");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should navigate clicks internally to the app", () => {
|
it("should navigate clicks internally to the app", () => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
const ROOM_NAME = "Test room";
|
const ROOM_NAME = "Test room";
|
||||||
|
@ -43,12 +43,12 @@ const checkRoomSummaryCard = (name: string): Chainable<JQuery<HTMLElement>> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("RightPanel", () => {
|
describe("RightPanel", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, NAME).then(() =>
|
cy.initTestUser(homeserver, NAME).then(() =>
|
||||||
cy.window({ log: false }).then(() => {
|
cy.window({ log: false }).then(() => {
|
||||||
cy.createRoom({ name: ROOM_NAME });
|
cy.createRoom({ name: ROOM_NAME });
|
||||||
cy.createSpace({ name: SPACE_NAME });
|
cy.createSpace({ name: SPACE_NAME });
|
||||||
|
@ -58,7 +58,7 @@ describe("RightPanel", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("in rooms", () => {
|
describe("in rooms", () => {
|
||||||
|
|
|
@ -16,23 +16,23 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
|
|
||||||
describe("Room Directory", () => {
|
describe("Room Directory", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Ray");
|
cy.initTestUser(homeserver, "Ray");
|
||||||
cy.getBot(synapse, { displayName: "Paul" }).as("bot");
|
cy.getBot(homeserver, { displayName: "Paul" }).as("bot");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should allow admin to add alias & publish room to directory", () => {
|
it("should allow admin to add alias & publish room to directory", () => {
|
||||||
|
|
|
@ -18,34 +18,34 @@ limitations under the License.
|
||||||
|
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
|
|
||||||
describe("Room Directory", () => {
|
describe("Room Directory", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Alice");
|
cy.initTestUser(homeserver, "Alice");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should switch between existing dm rooms without a loader", () => {
|
it("should switch between existing dm rooms without a loader", () => {
|
||||||
let bobClient: MatrixClient;
|
let bobClient: MatrixClient;
|
||||||
let charlieClient: MatrixClient;
|
let charlieClient: MatrixClient;
|
||||||
cy.getBot(synapse, {
|
cy.getBot(homeserver, {
|
||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
}).then((bob) => {
|
}).then((bob) => {
|
||||||
bobClient = bob;
|
bobClient = bob;
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.getBot(synapse, {
|
cy.getBot(homeserver, {
|
||||||
displayName: "Charlie",
|
displayName: "Charlie",
|
||||||
}).then((charlie) => {
|
}).then((charlie) => {
|
||||||
charlieClient = charlie;
|
charlieClient = charlie;
|
||||||
|
|
|
@ -16,34 +16,34 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import type { UserCredentials } from "../../support/login";
|
import type { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
describe("Device manager", () => {
|
describe("Device manager", () => {
|
||||||
let synapse: SynapseInstance | undefined;
|
let homeserver: HomeserverInstance | undefined;
|
||||||
let user: UserCredentials | undefined;
|
let user: UserCredentials | undefined;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.enableLabsFeature("feature_new_device_manager");
|
cy.enableLabsFeature("feature_new_device_manager");
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Alice")
|
cy.initTestUser(homeserver, "Alice")
|
||||||
.then((credentials) => {
|
.then((credentials) => {
|
||||||
user = credentials;
|
user = credentials;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// create some extra sessions to manage
|
// create some extra sessions to manage
|
||||||
return cy.loginUser(synapse, user.username, user.password);
|
return cy.loginUser(homeserver, user.username, user.password);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return cy.loginUser(synapse, user.username, user.password);
|
return cy.loginUser(homeserver, user.username, user.password);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse!);
|
cy.stopHomeserver(homeserver!);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should display sessions", () => {
|
it("should display sessions", () => {
|
||||||
|
|
|
@ -16,10 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
function seedLabs(synapse: SynapseInstance, labsVal: boolean | null): void {
|
function seedLabs(homeserver: HomeserverInstance, labsVal: boolean | null): void {
|
||||||
cy.initTestUser(synapse, "Sally", () => {
|
cy.initTestUser(homeserver, "Sally", () => {
|
||||||
// seed labs flag
|
// seed labs flag
|
||||||
cy.window({ log: false }).then((win) => {
|
cy.window({ log: false }).then((win) => {
|
||||||
if (typeof labsVal === "boolean") {
|
if (typeof labsVal === "boolean") {
|
||||||
|
@ -61,30 +61,30 @@ describe("Hidden Read Receipts Setting Migration", () => {
|
||||||
// For a security-sensitive feature like hidden read receipts, it's absolutely vital
|
// For a security-sensitive feature like hidden read receipts, it's absolutely vital
|
||||||
// that we migrate the setting appropriately.
|
// that we migrate the setting appropriately.
|
||||||
|
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not migrate the lack of a labs flag", () => {
|
it("should not migrate the lack of a labs flag", () => {
|
||||||
seedLabs(synapse, null);
|
seedLabs(homeserver, null);
|
||||||
testForVal(null);
|
testForVal(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should migrate labsHiddenRR=false as sendRR=true", () => {
|
it("should migrate labsHiddenRR=false as sendRR=true", () => {
|
||||||
seedLabs(synapse, false);
|
seedLabs(homeserver, false);
|
||||||
testForVal(true);
|
testForVal(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should migrate labsHiddenRR=true as sendRR=false", () => {
|
it("should migrate labsHiddenRR=true as sendRR=false", () => {
|
||||||
seedLabs(synapse, true);
|
seedLabs(homeserver, true);
|
||||||
testForVal(false);
|
testForVal(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,20 +20,21 @@ import _ from "lodash";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { Interception } from "cypress/types/net-stubbing";
|
import { Interception } from "cypress/types/net-stubbing";
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { Layout } from "../../../src/settings/enums/Layout";
|
import { Layout } from "../../../src/settings/enums/Layout";
|
||||||
import { ProxyInstance } from "../../plugins/sliding-sync";
|
import { ProxyInstance } from "../../plugins/sliding-sync";
|
||||||
|
|
||||||
describe("Sliding Sync", () => {
|
describe("Sliding Sync", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default")
|
cy.startHomeserver("default")
|
||||||
.as("synapse")
|
.as("homeserver")
|
||||||
.then((synapse) => {
|
.then((homeserver) => {
|
||||||
cy.startProxy(synapse).as("proxy");
|
cy.startProxy(homeserver).as("proxy");
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.all([cy.get<SynapseInstance>("@synapse"), cy.get<ProxyInstance>("@proxy")]).then(([synapse, proxy]) => {
|
cy.all([cy.get<HomeserverInstance>("@homeserver"), cy.get<ProxyInstance>("@proxy")]).then(
|
||||||
|
([homeserver, proxy]) => {
|
||||||
cy.enableLabsFeature("feature_sliding_sync");
|
cy.enableLabsFeature("feature_sliding_sync");
|
||||||
|
|
||||||
cy.intercept("/config.json?cachebuster=*", (req) => {
|
cy.intercept("/config.json?cachebuster=*", (req) => {
|
||||||
|
@ -47,16 +48,17 @@ describe("Sliding Sync", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Sloth").then(() => {
|
cy.initTestUser(homeserver, "Sloth").then(() => {
|
||||||
return cy.window({ log: false }).then(() => {
|
return cy.window({ log: false }).then(() => {
|
||||||
cy.createRoom({ name: "Test Room" }).as("roomId");
|
cy.createRoom({ name: "Test Room" }).as("roomId");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.get<SynapseInstance>("@synapse").then(cy.stopSynapse);
|
cy.get<HomeserverInstance>("@homeserver").then(cy.stopHomeserver);
|
||||||
cy.get<ProxyInstance>("@proxy").then(cy.stopProxy);
|
cy.get<ProxyInstance>("@proxy").then(cy.stopProxy);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,9 +86,9 @@ describe("Sliding Sync", () => {
|
||||||
};
|
};
|
||||||
const createAndJoinBob = () => {
|
const createAndJoinBob = () => {
|
||||||
// create a Bob user
|
// create a Bob user
|
||||||
cy.get<SynapseInstance>("@synapse").then((synapse) => {
|
cy.get<HomeserverInstance>("@homeserver").then((homeserver) => {
|
||||||
return cy
|
return cy
|
||||||
.getBot(synapse, {
|
.getBot(homeserver, {
|
||||||
displayName: "Bob",
|
displayName: "Bob",
|
||||||
})
|
})
|
||||||
.as("bob");
|
.as("bob");
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
import type { ICreateRoomOpts } from "matrix-js-sdk/src/@types/requests";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
|
@ -59,14 +59,14 @@ function spaceChildInitialState(roomId: string): ICreateRoomOpts["initial_state"
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Spaces", () => {
|
describe("Spaces", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let user: UserCredentials;
|
let user: UserCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Sue").then((_user) => {
|
cy.initTestUser(homeserver, "Sue").then((_user) => {
|
||||||
user = _user;
|
user = _user;
|
||||||
cy.mockClipboard();
|
cy.mockClipboard();
|
||||||
});
|
});
|
||||||
|
@ -74,7 +74,7 @@ describe("Spaces", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.only("should allow user to create public space", () => {
|
it.only("should allow user to create public space", () => {
|
||||||
|
@ -173,7 +173,7 @@ describe("Spaces", () => {
|
||||||
|
|
||||||
it("should allow user to invite another to a space", () => {
|
it("should allow user to invite another to a space", () => {
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
cy.getBot(synapse, { displayName: "BotBob" }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "BotBob" }).then((_bot) => {
|
||||||
bot = _bot;
|
bot = _bot;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -208,7 +208,7 @@ describe("Spaces", () => {
|
||||||
});
|
});
|
||||||
cy.getSpacePanelButton("My Space").should("exist");
|
cy.getSpacePanelButton("My Space").should("exist");
|
||||||
|
|
||||||
cy.getBot(synapse, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
|
cy.getBot(homeserver, { displayName: "BotBob" }).then({ timeout: 10000 }, async (bot) => {
|
||||||
const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space"));
|
const { room_id: roomId } = await bot.createRoom(spaceCreateOptions("Space Space"));
|
||||||
await bot.invite(roomId, user.userId);
|
await bot.invite(roomId, user.userId);
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import Loggable = Cypress.Loggable;
|
import Loggable = Cypress.Loggable;
|
||||||
import Timeoutable = Cypress.Timeoutable;
|
import Timeoutable = Cypress.Timeoutable;
|
||||||
|
@ -136,7 +136,7 @@ Cypress.Commands.add("startDM", (name: string) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Spotlight", () => {
|
describe("Spotlight", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
const bot1Name = "BotBob";
|
const bot1Name = "BotBob";
|
||||||
let bot1: MatrixClient;
|
let bot1: MatrixClient;
|
||||||
|
@ -154,16 +154,16 @@ describe("Spotlight", () => {
|
||||||
let room3Id: string;
|
let room3Id: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Jim")
|
cy.initTestUser(homeserver, "Jim")
|
||||||
.then(() =>
|
.then(() =>
|
||||||
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
|
cy.getBot(homeserver, { displayName: bot1Name }).then((_bot1) => {
|
||||||
bot1 = _bot1;
|
bot1 = _bot1;
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.then(() =>
|
.then(() =>
|
||||||
cy.getBot(synapse, { displayName: bot2Name }).then((_bot2) => {
|
cy.getBot(homeserver, { displayName: bot2Name }).then((_bot2) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
bot2 = _bot2;
|
bot2 = _bot2;
|
||||||
}),
|
}),
|
||||||
|
@ -205,7 +205,7 @@ describe("Spotlight", () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.visit("/#/home");
|
cy.visit("/#/home");
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should be able to add and remove filters via keyboard", () => {
|
it("should be able to add and remove filters via keyboard", () => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
|
|
||||||
function markWindowBeforeReload(): void {
|
function markWindowBeforeReload(): void {
|
||||||
|
@ -25,7 +25,7 @@ function markWindowBeforeReload(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Threads", () => {
|
describe("Threads", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
// Default threads to ON for this spec
|
// Default threads to ON for this spec
|
||||||
|
@ -33,15 +33,15 @@ describe("Threads", () => {
|
||||||
cy.window().then((win) => {
|
cy.window().then((win) => {
|
||||||
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
});
|
});
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Tom");
|
cy.initTestUser(homeserver, "Tom");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reload when enabling threads beta", () => {
|
it("should reload when enabling threads beta", () => {
|
||||||
|
@ -75,7 +75,7 @@ describe("Threads", () => {
|
||||||
|
|
||||||
it("should be usable for a conversation", () => {
|
it("should be usable for a conversation", () => {
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
cy.getBot(synapse, {
|
cy.getBot(homeserver, {
|
||||||
displayName: "BotBob",
|
displayName: "BotBob",
|
||||||
autoAcceptInvites: false,
|
autoAcceptInvites: false,
|
||||||
}).then((_bot) => {
|
}).then((_bot) => {
|
||||||
|
|
|
@ -16,11 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { MessageEvent } from "matrix-events-sdk";
|
|
||||||
|
|
||||||
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
import type { ISendEventResponse } from "matrix-js-sdk/src/@types/requests";
|
||||||
import type { EventType } from "matrix-js-sdk/src/@types/event";
|
import type { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
import { Layout } from "../../../src/settings/enums/Layout";
|
import { Layout } from "../../../src/settings/enums/Layout";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
@ -55,16 +53,21 @@ const expectAvatar = (e: JQuery<HTMLElement>, avatarUrl: string): void => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendEvent = (roomId: string, html = false): Chainable<ISendEventResponse> => {
|
const sendEvent = (roomId: string, html = false): Chainable<ISendEventResponse> => {
|
||||||
return cy.sendEvent(
|
const content = {
|
||||||
roomId,
|
msgtype: "m.text" as MsgType,
|
||||||
null,
|
body: "Message",
|
||||||
"m.room.message" as EventType,
|
format: undefined,
|
||||||
MessageEvent.from("Message", html ? "<b>Message</b>" : undefined).serialize().content,
|
formatted_body: undefined,
|
||||||
);
|
};
|
||||||
|
if (html) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = "<b>Message</b>";
|
||||||
|
}
|
||||||
|
return cy.sendEvent(roomId, null, "m.room.message" as EventType, content);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("Timeline", () => {
|
describe("Timeline", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
let roomId: string;
|
let roomId: string;
|
||||||
|
|
||||||
|
@ -72,9 +75,9 @@ describe("Timeline", () => {
|
||||||
let newAvatarUrl: string;
|
let newAvatarUrl: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, OLD_NAME).then(() =>
|
cy.initTestUser(homeserver, OLD_NAME).then(() =>
|
||||||
cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
|
cy.createRoom({ name: ROOM_NAME }).then((_room1Id) => {
|
||||||
roomId = _room1Id;
|
roomId = _room1Id;
|
||||||
}),
|
}),
|
||||||
|
@ -83,7 +86,7 @@ describe("Timeline", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("useOnlyCurrentProfiles", () => {
|
describe("useOnlyCurrentProfiles", () => {
|
||||||
|
@ -314,12 +317,10 @@ describe("Timeline", () => {
|
||||||
},
|
},
|
||||||
}).as("preview_url");
|
}).as("preview_url");
|
||||||
|
|
||||||
cy.sendEvent(
|
cy.sendEvent(roomId, null, "m.room.message" as EventType, {
|
||||||
roomId,
|
msgtype: "m.text" as MsgType,
|
||||||
null,
|
body: "https://call.element.io/",
|
||||||
"m.room.message" as EventType,
|
});
|
||||||
MessageEvent.from("https://call.element.io/").serialize().content,
|
|
||||||
);
|
|
||||||
cy.visit("/#/room/" + roomId);
|
cy.visit("/#/room/" + roomId);
|
||||||
|
|
||||||
cy.get(".mx_LinkPreviewWidget").should("exist").should("contain.text", "Element Call");
|
cy.get(".mx_LinkPreviewWidget").should("exist").should("contain.text", "Element Call");
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
function assertNoToasts(): void {
|
function assertNoToasts(): void {
|
||||||
|
@ -40,10 +40,10 @@ function rejectToast(expectedTitle: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Analytics Toast", () => {
|
describe("Analytics Toast", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not show an analytics toast if config has nothing about posthog", () => {
|
it("should not show an analytics toast if config has nothing about posthog", () => {
|
||||||
|
@ -55,9 +55,9 @@ describe("Analytics Toast", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Tod");
|
cy.initTestUser(homeserver, "Tod");
|
||||||
});
|
});
|
||||||
|
|
||||||
rejectToast("Notifications");
|
rejectToast("Notifications");
|
||||||
|
@ -78,9 +78,9 @@ describe("Analytics Toast", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Tod");
|
cy.initTestUser(homeserver, "Tod");
|
||||||
rejectToast("Notifications");
|
rejectToast("Notifications");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,19 +16,19 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("Update", () => {
|
describe("Update", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should navigate to ?updated=$VERSION if realises it is immediately out of date on load", () => {
|
it("should navigate to ?updated=$VERSION if realises it is immediately out of date on load", () => {
|
||||||
|
@ -42,7 +42,7 @@ describe("Update", () => {
|
||||||
},
|
},
|
||||||
}).as("version");
|
}).as("version");
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Ursa");
|
cy.initTestUser(homeserver, "Ursa");
|
||||||
|
|
||||||
cy.wait("@version");
|
cy.wait("@version");
|
||||||
cy.url()
|
cy.url()
|
||||||
|
|
|
@ -16,25 +16,25 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import type { UserCredentials } from "../../support/login";
|
import type { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
describe("User Menu", () => {
|
describe("User Menu", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let user: UserCredentials;
|
let user: UserCredentials;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Jeff").then((credentials) => {
|
cy.initTestUser(homeserver, "Jeff").then((credentials) => {
|
||||||
user = credentials;
|
user = credentials;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should contain our name & userId", () => {
|
it("should contain our name & userId", () => {
|
||||||
|
|
|
@ -17,18 +17,18 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("User Onboarding (new user)", () => {
|
describe("User Onboarding (new user)", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
const bot1Name = "BotBob";
|
const bot1Name = "BotBob";
|
||||||
let bot1: MatrixClient;
|
let bot1: MatrixClient;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Jane Doe");
|
cy.initTestUser(homeserver, "Jane Doe");
|
||||||
cy.window({ log: false }).then((win) => {
|
cy.window({ log: false }).then((win) => {
|
||||||
win.localStorage.setItem("mx_registration_time", "1656633601");
|
win.localStorage.setItem("mx_registration_time", "1656633601");
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,7 @@ describe("User Onboarding (new user)", () => {
|
||||||
// wait for the app to load
|
// wait for the app to load
|
||||||
return cy.get(".mx_MatrixChat", { timeout: 15000 });
|
return cy.get(".mx_MatrixChat", { timeout: 15000 });
|
||||||
});
|
});
|
||||||
cy.getBot(synapse, { displayName: bot1Name }).then((_bot1) => {
|
cy.getBot(homeserver, { displayName: bot1Name }).then((_bot1) => {
|
||||||
bot1 = _bot1;
|
bot1 = _bot1;
|
||||||
});
|
});
|
||||||
cy.get(".mx_UserOnboardingPage").should("exist");
|
cy.get(".mx_UserOnboardingPage").should("exist");
|
||||||
|
@ -51,7 +51,7 @@ describe("User Onboarding (new user)", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("page is shown and preference exists", () => {
|
it("page is shown and preference exists", () => {
|
||||||
|
|
|
@ -16,15 +16,15 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
describe("User Onboarding (old user)", () => {
|
describe("User Onboarding (old user)", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
cy.initTestUser(synapse, "Jane Doe");
|
cy.initTestUser(homeserver, "Jane Doe");
|
||||||
cy.window({ log: false }).then((win) => {
|
cy.window({ log: false }).then((win) => {
|
||||||
win.localStorage.setItem("mx_registration_time", "2");
|
win.localStorage.setItem("mx_registration_time", "2");
|
||||||
});
|
});
|
||||||
|
@ -37,7 +37,7 @@ describe("User Onboarding (old user)", () => {
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.visit("/#/home");
|
cy.visit("/#/home");
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("page and preference are hidden", () => {
|
it("page and preference are hidden", () => {
|
||||||
|
|
|
@ -16,23 +16,23 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { MatrixClient } from "../../global";
|
import { MatrixClient } from "../../global";
|
||||||
|
|
||||||
describe("UserView", () => {
|
describe("UserView", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Violet");
|
cy.initTestUser(homeserver, "Violet");
|
||||||
cy.getBot(synapse, { displayName: "Usman" }).as("bot");
|
cy.getBot(homeserver, { displayName: "Usman" }).as("bot");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the user view as expected", () => {
|
it("should render the user view as expected", () => {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { IWidget } from "matrix-widget-api";
|
import { IWidget } from "matrix-widget-api";
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
const ROOM_NAME = "Test Room";
|
const ROOM_NAME = "Test Room";
|
||||||
const WIDGET_ID = "fake-widget";
|
const WIDGET_ID = "fake-widget";
|
||||||
|
@ -34,14 +34,14 @@ const WIDGET_HTML = `
|
||||||
|
|
||||||
describe("Widget Layout", () => {
|
describe("Widget Layout", () => {
|
||||||
let widgetUrl: string;
|
let widgetUrl: string;
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let roomId: string;
|
let roomId: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Sally");
|
cy.initTestUser(homeserver, "Sally");
|
||||||
});
|
});
|
||||||
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
|
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
|
||||||
widgetUrl = url;
|
widgetUrl = url;
|
||||||
|
@ -91,7 +91,7 @@ describe("Widget Layout", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
|
|
||||||
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
|
const STICKER_PICKER_WIDGET_ID = "fake-sticker-picker";
|
||||||
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
|
const STICKER_PICKER_WIDGET_NAME = "Fake Stickers";
|
||||||
|
@ -102,13 +102,13 @@ describe("Stickers", () => {
|
||||||
// See sendStickerFromPicker() for more detail on iframe comms.
|
// See sendStickerFromPicker() for more detail on iframe comms.
|
||||||
|
|
||||||
let stickerPickerUrl: string;
|
let stickerPickerUrl: string;
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Sally");
|
cy.initTestUser(homeserver, "Sally");
|
||||||
});
|
});
|
||||||
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
|
cy.serveHtmlFile(WIDGET_HTML).then((url) => {
|
||||||
stickerPickerUrl = url;
|
stickerPickerUrl = url;
|
||||||
|
@ -116,7 +116,7 @@ describe("Stickers", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ limitations under the License.
|
||||||
import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
|
import { IWidget } from "matrix-widget-api/src/interfaces/IWidget";
|
||||||
|
|
||||||
import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import type { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import { SynapseInstance } from "../../plugins/synapsedocker";
|
import { HomeserverInstance } from "../../plugins/utils/homeserver";
|
||||||
import { UserCredentials } from "../../support/login";
|
import { UserCredentials } from "../../support/login";
|
||||||
|
|
||||||
const DEMO_WIDGET_ID = "demo-widget-id";
|
const DEMO_WIDGET_ID = "demo-widget-id";
|
||||||
|
@ -90,7 +90,7 @@ function waitForRoomWidget(win: Cypress.AUTWindow, widgetId: string, roomId: str
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("Widget PIP", () => {
|
describe("Widget PIP", () => {
|
||||||
let synapse: SynapseInstance;
|
let homeserver: HomeserverInstance;
|
||||||
let user: UserCredentials;
|
let user: UserCredentials;
|
||||||
let bot: MatrixClient;
|
let bot: MatrixClient;
|
||||||
let demoWidgetUrl: string;
|
let demoWidgetUrl: string;
|
||||||
|
@ -173,13 +173,13 @@ describe("Widget PIP", () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then((data) => {
|
cy.startHomeserver("default").then((data) => {
|
||||||
synapse = data;
|
homeserver = data;
|
||||||
|
|
||||||
cy.initTestUser(synapse, "Mike").then((_user) => {
|
cy.initTestUser(homeserver, "Mike").then((_user) => {
|
||||||
user = _user;
|
user = _user;
|
||||||
});
|
});
|
||||||
cy.getBot(synapse, { displayName: "Bot", autoAcceptInvites: false }).then((_bot) => {
|
cy.getBot(homeserver, { displayName: "Bot", autoAcceptInvites: false }).then((_bot) => {
|
||||||
bot = _bot;
|
bot = _bot;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -189,7 +189,7 @@ describe("Widget PIP", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
cy.stopWebServers();
|
cy.stopWebServers();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
181
cypress/plugins/dendritedocker/index.ts
Normal file
181
cypress/plugins/dendritedocker/index.ts
Normal file
|
@ -0,0 +1,181 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import * as path from "path";
|
||||||
|
import * as os from "os";
|
||||||
|
import * as crypto from "crypto";
|
||||||
|
import * as fse from "fs-extra";
|
||||||
|
|
||||||
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
|
import { getFreePort } from "../utils/port";
|
||||||
|
import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
|
||||||
|
import { HomeserverConfig, HomeserverInstance } from "../utils/homeserver";
|
||||||
|
|
||||||
|
// A cypress plugins to add command to start & stop dendrites in
|
||||||
|
// docker with preset templates.
|
||||||
|
|
||||||
|
const dendrites = new Map<string, HomeserverInstance>();
|
||||||
|
|
||||||
|
function randB64Bytes(numBytes: number): string {
|
||||||
|
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cfgDirFromTemplate(template: string): Promise<HomeserverConfig> {
|
||||||
|
template = "default";
|
||||||
|
const templateDir = path.join(__dirname, "templates", template);
|
||||||
|
const configFile = "dendrite.yaml";
|
||||||
|
|
||||||
|
const stats = await fse.stat(templateDir);
|
||||||
|
if (!stats?.isDirectory) {
|
||||||
|
throw new Error(`No such template: ${template}`);
|
||||||
|
}
|
||||||
|
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), "react-sdk-dendritedocker-"));
|
||||||
|
|
||||||
|
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
|
||||||
|
console.log(`Copy ${templateDir} -> ${tempDir}`);
|
||||||
|
await fse.copy(templateDir, tempDir, { filter: (f) => path.basename(f) !== configFile });
|
||||||
|
|
||||||
|
const registrationSecret = randB64Bytes(16);
|
||||||
|
|
||||||
|
const port = await getFreePort();
|
||||||
|
const baseUrl = `http://localhost:${port}`;
|
||||||
|
|
||||||
|
// now copy homeserver.yaml, applying substitutions
|
||||||
|
console.log(`Gen ${path.join(templateDir, configFile)}`);
|
||||||
|
let hsYaml = await fse.readFile(path.join(templateDir, configFile), "utf8");
|
||||||
|
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
|
||||||
|
await fse.writeFile(path.join(tempDir, configFile), hsYaml);
|
||||||
|
|
||||||
|
await dockerRun({
|
||||||
|
image: "matrixdotorg/dendrite-monolith:main",
|
||||||
|
params: ["--rm", "--entrypoint=", "-v", `${tempDir}:/mnt`],
|
||||||
|
containerName: `react-sdk-cypress-dendrite-keygen`,
|
||||||
|
cmd: ["/usr/bin/generate-keys", "-private-key", "/mnt/matrix_key.pem"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
port,
|
||||||
|
baseUrl,
|
||||||
|
configDir: tempDir,
|
||||||
|
registrationSecret,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a dendrite instance: the template must be the name of
|
||||||
|
// one of the templates in the cypress/plugins/dendritedocker/templates
|
||||||
|
// directory
|
||||||
|
async function dendriteStart(template: string): Promise<HomeserverInstance> {
|
||||||
|
const denCfg = await cfgDirFromTemplate(template);
|
||||||
|
|
||||||
|
console.log(`Starting dendrite with config dir ${denCfg.configDir}...`);
|
||||||
|
|
||||||
|
const dendriteId = await dockerRun({
|
||||||
|
image: "matrixdotorg/dendrite-monolith:main",
|
||||||
|
params: [
|
||||||
|
"--rm",
|
||||||
|
"-v",
|
||||||
|
`${denCfg.configDir}:/etc/dendrite`,
|
||||||
|
"-p",
|
||||||
|
`${denCfg.port}:8008/tcp`,
|
||||||
|
"--entrypoint",
|
||||||
|
"/usr/bin/dendrite-monolith-server",
|
||||||
|
],
|
||||||
|
containerName: `react-sdk-cypress-dendrite`,
|
||||||
|
cmd: ["--really-enable-open-registration", "true", "run"],
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Started dendrite with id ${dendriteId} on port ${denCfg.port}.`);
|
||||||
|
|
||||||
|
// Await Dendrite healthcheck
|
||||||
|
await dockerExec({
|
||||||
|
containerId: dendriteId,
|
||||||
|
params: [
|
||||||
|
"curl",
|
||||||
|
"--connect-timeout",
|
||||||
|
"30",
|
||||||
|
"--retry",
|
||||||
|
"30",
|
||||||
|
"--retry-delay",
|
||||||
|
"1",
|
||||||
|
"--retry-all-errors",
|
||||||
|
"--silent",
|
||||||
|
"http://localhost:8008/_matrix/client/versions",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const dendrite: HomeserverInstance = { serverId: dendriteId, ...denCfg };
|
||||||
|
dendrites.set(dendriteId, dendrite);
|
||||||
|
return dendrite;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dendriteStop(id: string): Promise<void> {
|
||||||
|
const denCfg = dendrites.get(id);
|
||||||
|
|
||||||
|
if (!denCfg) throw new Error("Unknown dendrite ID");
|
||||||
|
|
||||||
|
const dendriteLogsPath = path.join("cypress", "dendritelogs", id);
|
||||||
|
await fse.ensureDir(dendriteLogsPath);
|
||||||
|
|
||||||
|
await dockerLogs({
|
||||||
|
containerId: id,
|
||||||
|
stdoutFile: path.join(dendriteLogsPath, "stdout.log"),
|
||||||
|
stderrFile: path.join(dendriteLogsPath, "stderr.log"),
|
||||||
|
});
|
||||||
|
|
||||||
|
await dockerStop({
|
||||||
|
containerId: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await fse.remove(denCfg.configDir);
|
||||||
|
|
||||||
|
dendrites.delete(id);
|
||||||
|
|
||||||
|
console.log(`Stopped dendrite id ${id}.`);
|
||||||
|
// cypress deliberately fails if you return 'undefined', so
|
||||||
|
// return null to signal all is well, and we've handled the task.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Cypress.PluginConfig}
|
||||||
|
*/
|
||||||
|
export function dendriteDocker(on: PluginEvents, config: PluginConfigOptions) {
|
||||||
|
on("task", {
|
||||||
|
dendriteStart,
|
||||||
|
dendriteStop,
|
||||||
|
});
|
||||||
|
|
||||||
|
on("after:spec", async (spec) => {
|
||||||
|
// Cleans up any remaining dendrite instances after a spec run
|
||||||
|
// This is on the theory that we should avoid re-using dendrite
|
||||||
|
// instances between spec runs: they should be cheap enough to
|
||||||
|
// start that we can have a separate one for each spec run or even
|
||||||
|
// test. If we accidentally re-use dendrites, we could inadvertently
|
||||||
|
// make our tests depend on each other.
|
||||||
|
for (const denId of dendrites.keys()) {
|
||||||
|
console.warn(`Cleaning up dendrite ID ${denId} after ${spec.name}`);
|
||||||
|
await dendriteStop(denId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on("before:run", async () => {
|
||||||
|
// tidy up old dendrite log files before each run
|
||||||
|
await fse.emptyDir(path.join("cypress", "dendritelogs"));
|
||||||
|
});
|
||||||
|
}
|
374
cypress/plugins/dendritedocker/templates/default/dendrite.yaml
Normal file
374
cypress/plugins/dendritedocker/templates/default/dendrite.yaml
Normal file
|
@ -0,0 +1,374 @@
|
||||||
|
# This is the Dendrite configuration file.
|
||||||
|
#
|
||||||
|
# The configuration is split up into sections - each Dendrite component has a
|
||||||
|
# configuration section, in addition to the "global" section which applies to
|
||||||
|
# all components.
|
||||||
|
|
||||||
|
# The version of the configuration file.
|
||||||
|
version: 2
|
||||||
|
|
||||||
|
# Global Matrix configuration. This configuration applies to all components.
|
||||||
|
global:
|
||||||
|
# The domain name of this homeserver.
|
||||||
|
server_name: localhost
|
||||||
|
|
||||||
|
# The path to the signing private key file, used to sign requests and events.
|
||||||
|
# Note that this is NOT the same private key as used for TLS! To generate a
|
||||||
|
# signing key, use "./bin/generate-keys --private-key matrix_key.pem".
|
||||||
|
private_key: matrix_key.pem
|
||||||
|
|
||||||
|
# The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
|
||||||
|
# to old signing keys that were formerly in use on this domain name. These
|
||||||
|
# keys will not be used for federation request or event signing, but will be
|
||||||
|
# provided to any other homeserver that asks when trying to verify old events.
|
||||||
|
old_private_keys:
|
||||||
|
# If the old private key file is available:
|
||||||
|
# - private_key: old_matrix_key.pem
|
||||||
|
# expired_at: 1601024554498
|
||||||
|
# If only the public key (in base64 format) and key ID are known:
|
||||||
|
# - public_key: mn59Kxfdq9VziYHSBzI7+EDPDcBS2Xl7jeUdiiQcOnM=
|
||||||
|
# key_id: ed25519:mykeyid
|
||||||
|
# expired_at: 1601024554498
|
||||||
|
|
||||||
|
# How long a remote server can cache our server signing key before requesting it
|
||||||
|
# again. Increasing this number will reduce the number of requests made by other
|
||||||
|
# servers for our key but increases the period that a compromised key will be
|
||||||
|
# considered valid by other homeservers.
|
||||||
|
key_validity_period: 168h0m0s
|
||||||
|
|
||||||
|
# Global database connection pool, for PostgreSQL monolith deployments only. If
|
||||||
|
# this section is populated then you can omit the "database" blocks in all other
|
||||||
|
# sections. For polylith deployments, or monolith deployments using SQLite databases,
|
||||||
|
# you must configure the "database" block for each component instead.
|
||||||
|
# database:
|
||||||
|
# connection_string: postgresql://username:password@hostname/dendrite?sslmode=disable
|
||||||
|
# max_open_conns: 90
|
||||||
|
# max_idle_conns: 5
|
||||||
|
# conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Configuration for in-memory caches. Caches can often improve performance by
|
||||||
|
# keeping frequently accessed items (like events, identifiers etc.) in memory
|
||||||
|
# rather than having to read them from the database.
|
||||||
|
cache:
|
||||||
|
# The estimated maximum size for the global cache in bytes, or in terabytes,
|
||||||
|
# gigabytes, megabytes or kilobytes when the appropriate 'tb', 'gb', 'mb' or
|
||||||
|
# 'kb' suffix is specified. Note that this is not a hard limit, nor is it a
|
||||||
|
# memory limit for the entire process. A cache that is too small may ultimately
|
||||||
|
# provide little or no benefit.
|
||||||
|
max_size_estimated: 1gb
|
||||||
|
|
||||||
|
# The maximum amount of time that a cache entry can live for in memory before
|
||||||
|
# it will be evicted and/or refreshed from the database. Lower values result in
|
||||||
|
# easier admission of new cache entries but may also increase database load in
|
||||||
|
# comparison to higher values, so adjust conservatively. Higher values may make
|
||||||
|
# it harder for new items to make it into the cache, e.g. if new rooms suddenly
|
||||||
|
# become popular.
|
||||||
|
max_age: 1h
|
||||||
|
|
||||||
|
# The server name to delegate server-server communications to, with optional port
|
||||||
|
# e.g. localhost:443
|
||||||
|
well_known_server_name: ""
|
||||||
|
|
||||||
|
# The server name to delegate client-server communications to, with optional port
|
||||||
|
# e.g. localhost:443
|
||||||
|
well_known_client_name: ""
|
||||||
|
|
||||||
|
# Lists of domains that the server will trust as identity servers to verify third
|
||||||
|
# party identifiers such as phone numbers and email addresses.
|
||||||
|
trusted_third_party_id_servers:
|
||||||
|
- matrix.org
|
||||||
|
- vector.im
|
||||||
|
|
||||||
|
# Disables federation. Dendrite will not be able to communicate with other servers
|
||||||
|
# in the Matrix federation and the federation API will not be exposed.
|
||||||
|
disable_federation: false
|
||||||
|
|
||||||
|
# Configures the handling of presence events. Inbound controls whether we receive
|
||||||
|
# presence events from other servers, outbound controls whether we send presence
|
||||||
|
# events for our local users to other servers.
|
||||||
|
presence:
|
||||||
|
enable_inbound: false
|
||||||
|
enable_outbound: false
|
||||||
|
|
||||||
|
# Configures phone-home statistics reporting. These statistics contain the server
|
||||||
|
# name, number of active users and some information on your deployment config.
|
||||||
|
# We use this information to understand how Dendrite is being used in the wild.
|
||||||
|
report_stats:
|
||||||
|
enabled: false
|
||||||
|
endpoint: https://matrix.org/report-usage-stats/push
|
||||||
|
|
||||||
|
# Server notices allows server admins to send messages to all users on the server.
|
||||||
|
server_notices:
|
||||||
|
enabled: false
|
||||||
|
# The local part, display name and avatar URL (as a mxc:// URL) for the user that
|
||||||
|
# will send the server notices. These are visible to all users on the deployment.
|
||||||
|
local_part: "_server"
|
||||||
|
display_name: "Server Alerts"
|
||||||
|
avatar_url: ""
|
||||||
|
# The room name to be used when sending server notices. This room name will
|
||||||
|
# appear in user clients.
|
||||||
|
room_name: "Server Alerts"
|
||||||
|
|
||||||
|
# Configuration for NATS JetStream
|
||||||
|
jetstream:
|
||||||
|
# A list of NATS Server addresses to connect to. If none are specified, an
|
||||||
|
# internal NATS server will be started automatically when running Dendrite in
|
||||||
|
# monolith mode. For polylith deployments, it is required to specify the address
|
||||||
|
# of at least one NATS Server node.
|
||||||
|
addresses:
|
||||||
|
# - localhost:4222
|
||||||
|
|
||||||
|
# Disable the validation of TLS certificates of NATS. This is
|
||||||
|
# not recommended in production since it may allow NATS traffic
|
||||||
|
# to be sent to an insecure endpoint.
|
||||||
|
disable_tls_validation: false
|
||||||
|
|
||||||
|
# Persistent directory to store JetStream streams in. This directory should be
|
||||||
|
# preserved across Dendrite restarts.
|
||||||
|
storage_path: ./
|
||||||
|
|
||||||
|
# The prefix to use for stream names for this homeserver - really only useful
|
||||||
|
# if you are running more than one Dendrite server on the same NATS deployment.
|
||||||
|
topic_prefix: Dendrite
|
||||||
|
|
||||||
|
# Configuration for Prometheus metric collection.
|
||||||
|
metrics:
|
||||||
|
enabled: false
|
||||||
|
basic_auth:
|
||||||
|
username: metrics
|
||||||
|
password: metrics
|
||||||
|
|
||||||
|
# Optional DNS cache. The DNS cache may reduce the load on DNS servers if there
|
||||||
|
# is no local caching resolver available for use.
|
||||||
|
dns_cache:
|
||||||
|
enabled: false
|
||||||
|
cache_size: 256
|
||||||
|
cache_lifetime: "5m" # 5 minutes; https://pkg.go.dev/time@master#ParseDuration
|
||||||
|
|
||||||
|
# Configuration for the Appservice API.
|
||||||
|
app_service_api:
|
||||||
|
# Disable the validation of TLS certificates of appservices. This is
|
||||||
|
# not recommended in production since it may allow appservice traffic
|
||||||
|
# to be sent to an insecure endpoint.
|
||||||
|
disable_tls_validation: false
|
||||||
|
|
||||||
|
# Appservice configuration files to load into this homeserver.
|
||||||
|
config_files:
|
||||||
|
# - /path/to/appservice_registration.yaml
|
||||||
|
|
||||||
|
# Configuration for the Client API.
|
||||||
|
client_api:
|
||||||
|
# Prevents new users from being able to register on this homeserver, except when
|
||||||
|
# using the registration shared secret below.
|
||||||
|
registration_disabled: false
|
||||||
|
|
||||||
|
# Prevents new guest accounts from being created. Guest registration is also
|
||||||
|
# disabled implicitly by setting 'registration_disabled' above.
|
||||||
|
guests_disabled: true
|
||||||
|
|
||||||
|
# If set, allows registration by anyone who knows the shared secret, regardless
|
||||||
|
# of whether registration is otherwise disabled.
|
||||||
|
registration_shared_secret: "{{REGISTRATION_SECRET}}"
|
||||||
|
|
||||||
|
# Whether to require reCAPTCHA for registration. If you have enabled registration
|
||||||
|
# then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used
|
||||||
|
# for coordinated spam attacks.
|
||||||
|
enable_registration_captcha: false
|
||||||
|
|
||||||
|
# Settings for ReCAPTCHA.
|
||||||
|
recaptcha_public_key: ""
|
||||||
|
recaptcha_private_key: ""
|
||||||
|
recaptcha_bypass_secret: ""
|
||||||
|
|
||||||
|
# To use hcaptcha.com instead of ReCAPTCHA, set the following parameters, otherwise just keep them empty.
|
||||||
|
# recaptcha_siteverify_api: "https://hcaptcha.com/siteverify"
|
||||||
|
# recaptcha_api_js_url: "https://js.hcaptcha.com/1/api.js"
|
||||||
|
# recaptcha_form_field: "h-captcha-response"
|
||||||
|
# recaptcha_sitekey_class: "h-captcha"
|
||||||
|
|
||||||
|
# TURN server information that this homeserver should send to clients.
|
||||||
|
turn:
|
||||||
|
turn_user_lifetime: "5m"
|
||||||
|
turn_uris:
|
||||||
|
# - turn:turn.server.org?transport=udp
|
||||||
|
# - turn:turn.server.org?transport=tcp
|
||||||
|
turn_shared_secret: ""
|
||||||
|
# If your TURN server requires static credentials, then you will need to enter
|
||||||
|
# them here instead of supplying a shared secret. Note that these credentials
|
||||||
|
# will be visible to clients!
|
||||||
|
# turn_username: ""
|
||||||
|
# turn_password: ""
|
||||||
|
|
||||||
|
# Settings for rate-limited endpoints. Rate limiting kicks in after the threshold
|
||||||
|
# number of "slots" have been taken by requests from a specific host. Each "slot"
|
||||||
|
# will be released after the cooloff time in milliseconds. Server administrators
|
||||||
|
# and appservice users are exempt from rate limiting by default.
|
||||||
|
rate_limiting:
|
||||||
|
enabled: true
|
||||||
|
threshold: 20
|
||||||
|
cooloff_ms: 500
|
||||||
|
exempt_user_ids:
|
||||||
|
# - "@user:domain.com"
|
||||||
|
|
||||||
|
# Configuration for the Federation API.
|
||||||
|
federation_api:
|
||||||
|
# How many times we will try to resend a failed transaction to a specific server. The
|
||||||
|
# backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. Once
|
||||||
|
# the max retries are exceeded, Dendrite will no longer try to send transactions to
|
||||||
|
# that server until it comes back to life and connects to us again.
|
||||||
|
send_max_retries: 16
|
||||||
|
|
||||||
|
# Disable the validation of TLS certificates of remote federated homeservers. Do not
|
||||||
|
# enable this option in production as it presents a security risk!
|
||||||
|
disable_tls_validation: false
|
||||||
|
|
||||||
|
# Disable HTTP keepalives, which also prevents connection reuse. Dendrite will typically
|
||||||
|
# keep HTTP connections open to remote hosts for 5 minutes as they can be reused much
|
||||||
|
# more quickly than opening new connections each time. Disabling keepalives will close
|
||||||
|
# HTTP connections immediately after a successful request but may result in more CPU and
|
||||||
|
# memory being used on TLS handshakes for each new connection instead.
|
||||||
|
disable_http_keepalives: false
|
||||||
|
|
||||||
|
# Perspective keyservers to use as a backup when direct key fetches fail. This may
|
||||||
|
# be required to satisfy key requests for servers that are no longer online when
|
||||||
|
# joining some rooms.
|
||||||
|
key_perspectives:
|
||||||
|
- server_name: matrix.org
|
||||||
|
keys:
|
||||||
|
- key_id: ed25519:auto
|
||||||
|
public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
|
||||||
|
- key_id: ed25519:a_RXGa
|
||||||
|
public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ
|
||||||
|
|
||||||
|
# This option will control whether Dendrite will prefer to look up keys directly
|
||||||
|
# or whether it should try perspective servers first, using direct fetches as a
|
||||||
|
# last resort.
|
||||||
|
prefer_direct_fetch: false
|
||||||
|
|
||||||
|
database:
|
||||||
|
connection_string: file:dendrite-federationapi.db
|
||||||
|
|
||||||
|
# Configuration for the Media API.
|
||||||
|
media_api:
|
||||||
|
# Storage path for uploaded media. May be relative or absolute.
|
||||||
|
base_path: ./media_store
|
||||||
|
|
||||||
|
# The maximum allowed file size (in bytes) for media uploads to this homeserver
|
||||||
|
# (0 = unlimited). If using a reverse proxy, ensure it allows requests at least
|
||||||
|
#this large (e.g. the client_max_body_size setting in nginx).
|
||||||
|
max_file_size_bytes: 10485760
|
||||||
|
|
||||||
|
# Whether to dynamically generate thumbnails if needed.
|
||||||
|
dynamic_thumbnails: false
|
||||||
|
|
||||||
|
# The maximum number of simultaneous thumbnail generators to run.
|
||||||
|
max_thumbnail_generators: 10
|
||||||
|
|
||||||
|
# A list of thumbnail sizes to be generated for media content.
|
||||||
|
thumbnail_sizes:
|
||||||
|
- width: 32
|
||||||
|
height: 32
|
||||||
|
method: crop
|
||||||
|
- width: 96
|
||||||
|
height: 96
|
||||||
|
method: crop
|
||||||
|
- width: 640
|
||||||
|
height: 480
|
||||||
|
method: scale
|
||||||
|
|
||||||
|
database:
|
||||||
|
connection_string: file:dendrite-mediaapi.db
|
||||||
|
|
||||||
|
# Configuration for enabling experimental MSCs on this homeserver.
|
||||||
|
mscs:
|
||||||
|
mscs:
|
||||||
|
# - msc2836 # (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836)
|
||||||
|
# - msc2946 # (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946)
|
||||||
|
|
||||||
|
database:
|
||||||
|
connection_string: file:dendrite-msc.db
|
||||||
|
|
||||||
|
# Configuration for the Sync API.
|
||||||
|
sync_api:
|
||||||
|
# This option controls which HTTP header to inspect to find the real remote IP
|
||||||
|
# address of the client. This is likely required if Dendrite is running behind
|
||||||
|
# a reverse proxy server.
|
||||||
|
# real_ip_header: X-Real-IP
|
||||||
|
|
||||||
|
# Configuration for the full-text search engine.
|
||||||
|
search:
|
||||||
|
# Whether or not search is enabled.
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# The path where the search index will be created in.
|
||||||
|
index_path: "./searchindex"
|
||||||
|
|
||||||
|
# The language most likely to be used on the server - used when indexing, to
|
||||||
|
# ensure the returned results match expectations. A full list of possible languages
|
||||||
|
# can be found at https://github.com/blevesearch/bleve/tree/master/analysis/lang
|
||||||
|
language: "en"
|
||||||
|
|
||||||
|
database:
|
||||||
|
connection_string: file:dendrite-syncapi.db
|
||||||
|
|
||||||
|
# Configuration for the User API.
|
||||||
|
user_api:
|
||||||
|
# The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31
|
||||||
|
# See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information.
|
||||||
|
# Setting this lower makes registration/login consume less CPU resources at the cost
|
||||||
|
# of security should the database be compromised. Setting this higher makes registration/login
|
||||||
|
# consume more CPU resources but makes it harder to brute force password hashes. This value
|
||||||
|
# can be lowered if performing tests or on embedded Dendrite instances (e.g WASM builds).
|
||||||
|
bcrypt_cost: 10
|
||||||
|
|
||||||
|
# The length of time that a token issued for a relying party from
|
||||||
|
# /_matrix/client/r0/user/{userId}/openid/request_token endpoint
|
||||||
|
# is considered to be valid in milliseconds.
|
||||||
|
# The default lifetime is 3600000ms (60 minutes).
|
||||||
|
# openid_token_lifetime_ms: 3600000
|
||||||
|
|
||||||
|
# Users who register on this homeserver will automatically be joined to the rooms listed under "auto_join_rooms" option.
|
||||||
|
# By default, any room aliases included in this list will be created as a publicly joinable room
|
||||||
|
# when the first user registers for the homeserver. If the room already exists,
|
||||||
|
# make certain it is a publicly joinable room, i.e. the join rule of the room must be set to 'public'.
|
||||||
|
# As Spaces are just rooms under the hood, Space aliases may also be used.
|
||||||
|
auto_join_rooms:
|
||||||
|
# - "#main:matrix.org"
|
||||||
|
|
||||||
|
account_database:
|
||||||
|
connection_string: file:dendrite-userapi.db
|
||||||
|
|
||||||
|
room_server:
|
||||||
|
database:
|
||||||
|
connection_string: file:dendrite-roomserverapi.db
|
||||||
|
|
||||||
|
key_server:
|
||||||
|
database:
|
||||||
|
connection_string: file:dendrite-keyserverapi.db
|
||||||
|
|
||||||
|
# Configuration for Opentracing.
|
||||||
|
# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on
|
||||||
|
# how this works and how to set it up.
|
||||||
|
tracing:
|
||||||
|
enabled: false
|
||||||
|
jaeger:
|
||||||
|
serviceName: ""
|
||||||
|
disabled: false
|
||||||
|
rpc_metrics: false
|
||||||
|
tags: []
|
||||||
|
sampler: null
|
||||||
|
reporter: null
|
||||||
|
headers: null
|
||||||
|
baggage_restrictions: null
|
||||||
|
throttler: null
|
||||||
|
|
||||||
|
# Logging configuration. The "std" logging type controls the logs being sent to
|
||||||
|
# stdout. The "file" logging type controls logs being written to a log folder on
|
||||||
|
# the disk. Supported log levels are "debug", "info", "warn", "error".
|
||||||
|
logging:
|
||||||
|
- type: std
|
||||||
|
level: debug
|
||||||
|
- type: file
|
||||||
|
level: debug
|
||||||
|
params:
|
||||||
|
path: ./logs
|
|
@ -30,7 +30,7 @@ export function dockerRun(opts: {
|
||||||
image: string;
|
image: string;
|
||||||
containerName: string;
|
containerName: string;
|
||||||
params?: string[];
|
params?: string[];
|
||||||
cmd?: string;
|
cmd?: string[];
|
||||||
}): Promise<string> {
|
}): Promise<string> {
|
||||||
const userInfo = os.userInfo();
|
const userInfo = os.userInfo();
|
||||||
const params = opts.params ?? [];
|
const params = opts.params ?? [];
|
||||||
|
@ -49,7 +49,7 @@ export function dockerRun(opts: {
|
||||||
opts.image,
|
opts.image,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (opts.cmd) args.push(opts.cmd);
|
if (opts.cmd) args.push(...opts.cmd);
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
return new Promise<string>((resolve, reject) => {
|
||||||
childProcess.execFile("docker", args, (err, stdout) => {
|
childProcess.execFile("docker", args, (err, stdout) => {
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
import PluginEvents = Cypress.PluginEvents;
|
import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
import { synapseDocker } from "./synapsedocker";
|
import { synapseDocker } from "./synapsedocker";
|
||||||
|
import { dendriteDocker } from "./dendritedocker";
|
||||||
import { slidingSyncProxyDocker } from "./sliding-sync";
|
import { slidingSyncProxyDocker } from "./sliding-sync";
|
||||||
import { webserver } from "./webserver";
|
import { webserver } from "./webserver";
|
||||||
import { docker } from "./docker";
|
import { docker } from "./docker";
|
||||||
|
@ -30,6 +31,7 @@ import { log } from "./log";
|
||||||
export default function (on: PluginEvents, config: PluginConfigOptions) {
|
export default function (on: PluginEvents, config: PluginConfigOptions) {
|
||||||
docker(on, config);
|
docker(on, config);
|
||||||
synapseDocker(on, config);
|
synapseDocker(on, config);
|
||||||
|
dendriteDocker(on, config);
|
||||||
slidingSyncProxyDocker(on, config);
|
slidingSyncProxyDocker(on, config);
|
||||||
webserver(on, config);
|
webserver(on, config);
|
||||||
log(on, config);
|
log(on, config);
|
||||||
|
|
|
@ -20,7 +20,7 @@ import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
import { dockerExec, dockerIp, dockerRun, dockerStop } from "../docker";
|
import { dockerExec, dockerIp, dockerRun, dockerStop } from "../docker";
|
||||||
import { getFreePort } from "../utils/port";
|
import { getFreePort } from "../utils/port";
|
||||||
import { SynapseInstance } from "../synapsedocker";
|
import { HomeserverInstance } from "../utils/homeserver";
|
||||||
|
|
||||||
// A cypress plugin to add command to start & stop https://github.com/matrix-org/sliding-sync
|
// 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-proxy` image.
|
||||||
|
@ -35,7 +35,7 @@ const instances = new Map<string, ProxyInstance>();
|
||||||
|
|
||||||
const PG_PASSWORD = "p4S5w0rD";
|
const PG_PASSWORD = "p4S5w0rD";
|
||||||
|
|
||||||
async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<ProxyInstance> {
|
async function proxyStart(dockerTag: string, homeserver: HomeserverInstance): Promise<ProxyInstance> {
|
||||||
console.log(new Date(), "Starting sliding sync proxy...");
|
console.log(new Date(), "Starting sliding sync proxy...");
|
||||||
|
|
||||||
const postgresId = await dockerRun({
|
const postgresId = await dockerRun({
|
||||||
|
@ -45,7 +45,7 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
|
||||||
});
|
});
|
||||||
|
|
||||||
const postgresIp = await dockerIp({ containerId: postgresId });
|
const postgresIp = await dockerIp({ containerId: postgresId });
|
||||||
const synapseIp = await dockerIp({ containerId: synapse.synapseId });
|
const homeserverIp = await dockerIp({ containerId: homeserver.serverId });
|
||||||
console.log(new Date(), "postgres container up");
|
console.log(new Date(), "postgres container up");
|
||||||
|
|
||||||
const waitTimeMillis = 30000;
|
const waitTimeMillis = 30000;
|
||||||
|
@ -81,7 +81,7 @@ async function proxyStart(dockerTag: string, synapse: SynapseInstance): Promise<
|
||||||
"-e",
|
"-e",
|
||||||
"SYNCV3_SECRET=bwahahaha",
|
"SYNCV3_SECRET=bwahahaha",
|
||||||
"-e",
|
"-e",
|
||||||
`SYNCV3_SERVER=http://${synapseIp}:8008`,
|
`SYNCV3_SERVER=http://${homeserverIp}:8008`,
|
||||||
"-e",
|
"-e",
|
||||||
`SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
|
`SYNCV3_DB=user=postgres dbname=postgres password=${PG_PASSWORD} host=${postgresIp} sslmode=disable`,
|
||||||
],
|
],
|
||||||
|
|
|
@ -25,29 +25,18 @@ import PluginEvents = Cypress.PluginEvents;
|
||||||
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
import PluginConfigOptions = Cypress.PluginConfigOptions;
|
||||||
import { getFreePort } from "../utils/port";
|
import { getFreePort } from "../utils/port";
|
||||||
import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
|
import { dockerExec, dockerLogs, dockerRun, dockerStop } from "../docker";
|
||||||
|
import { HomeserverConfig, HomeserverInstance } from "../utils/homeserver";
|
||||||
|
|
||||||
// A cypress plugins to add command to start & stop synapses in
|
// A cypress plugins to add command to start & stop synapses in
|
||||||
// docker with preset templates.
|
// docker with preset templates.
|
||||||
|
|
||||||
interface SynapseConfig {
|
const synapses = new Map<string, HomeserverInstance>();
|
||||||
configDir: string;
|
|
||||||
registrationSecret: string;
|
|
||||||
// Synapse must be configured with its public_baseurl so we have to allocate a port & url at this stage
|
|
||||||
baseUrl: string;
|
|
||||||
port: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SynapseInstance extends SynapseConfig {
|
|
||||||
synapseId: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const synapses = new Map<string, SynapseInstance>();
|
|
||||||
|
|
||||||
function randB64Bytes(numBytes: number): string {
|
function randB64Bytes(numBytes: number): string {
|
||||||
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
async function cfgDirFromTemplate(template: string): Promise<HomeserverConfig> {
|
||||||
const templateDir = path.join(__dirname, "templates", template);
|
const templateDir = path.join(__dirname, "templates", template);
|
||||||
|
|
||||||
const stats = await fse.stat(templateDir);
|
const stats = await fse.stat(templateDir);
|
||||||
|
@ -94,7 +83,7 @@ async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
|
||||||
// Start a synapse instance: the template must be the name of
|
// Start a synapse instance: the template must be the name of
|
||||||
// one of the templates in the cypress/plugins/synapsedocker/templates
|
// one of the templates in the cypress/plugins/synapsedocker/templates
|
||||||
// directory
|
// directory
|
||||||
async function synapseStart(template: string): Promise<SynapseInstance> {
|
async function synapseStart(template: string): Promise<HomeserverInstance> {
|
||||||
const synCfg = await cfgDirFromTemplate(template);
|
const synCfg = await cfgDirFromTemplate(template);
|
||||||
|
|
||||||
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
|
||||||
|
@ -103,7 +92,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
image: "matrixdotorg/synapse:develop",
|
image: "matrixdotorg/synapse:develop",
|
||||||
containerName: `react-sdk-cypress-synapse`,
|
containerName: `react-sdk-cypress-synapse`,
|
||||||
params: ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`],
|
params: ["--rm", "-v", `${synCfg.configDir}:/data`, "-p", `${synCfg.port}:8008/tcp`],
|
||||||
cmd: "run",
|
cmd: ["run"],
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
console.log(`Started synapse with id ${synapseId} on port ${synCfg.port}.`);
|
||||||
|
@ -125,7 +114,7 @@ async function synapseStart(template: string): Promise<SynapseInstance> {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
const synapse: SynapseInstance = { synapseId, ...synCfg };
|
const synapse: HomeserverInstance = { serverId: synapseId, ...synCfg };
|
||||||
synapses.set(synapseId, synapse);
|
synapses.set(synapseId, synapse);
|
||||||
return synapse;
|
return synapse;
|
||||||
}
|
}
|
||||||
|
|
28
cypress/plugins/utils/homeserver.ts
Normal file
28
cypress/plugins/utils/homeserver.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
export interface HomeserverConfig {
|
||||||
|
configDir: string;
|
||||||
|
registrationSecret: string;
|
||||||
|
baseUrl: string;
|
||||||
|
port: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HomeserverInstance extends HomeserverConfig {
|
||||||
|
serverId: string;
|
||||||
|
}
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
import type { ISendEventResponse, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
import { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||||
import { Credentials } from "./synapse";
|
import { Credentials } from "./homeserver";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
interface CreateBotOpts {
|
interface CreateBotOpts {
|
||||||
|
@ -61,19 +61,19 @@ declare global {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
/**
|
/**
|
||||||
* Returns a new Bot instance
|
* Returns a new Bot instance
|
||||||
* @param synapse the instance on which to register the bot user
|
* @param homeserver the instance on which to register the bot user
|
||||||
* @param opts create bot options
|
* @param opts create bot options
|
||||||
*/
|
*/
|
||||||
getBot(synapse: SynapseInstance, opts: CreateBotOpts): Chainable<CypressBot>;
|
getBot(homeserver: HomeserverInstance, opts: CreateBotOpts): Chainable<CypressBot>;
|
||||||
/**
|
/**
|
||||||
* Returns a new Bot instance logged in as an existing user
|
* Returns a new Bot instance logged in as an existing user
|
||||||
* @param synapse the instance on which to register the bot user
|
* @param homeserver the instance on which to register the bot user
|
||||||
* @param username the username for the bot to log in with
|
* @param username the username for the bot to log in with
|
||||||
* @param password the password for the bot to log in with
|
* @param password the password for the bot to log in with
|
||||||
* @param opts create bot options
|
* @param opts create bot options
|
||||||
*/
|
*/
|
||||||
loginBot(
|
loginBot(
|
||||||
synapse: SynapseInstance,
|
homeserver: HomeserverInstance,
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
opts: CreateBotOpts,
|
opts: CreateBotOpts,
|
||||||
|
@ -102,7 +102,7 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupBotClient(
|
function setupBotClient(
|
||||||
synapse: SynapseInstance,
|
homeserver: HomeserverInstance,
|
||||||
credentials: Credentials,
|
credentials: Credentials,
|
||||||
opts: CreateBotOpts,
|
opts: CreateBotOpts,
|
||||||
): Chainable<MatrixClient> {
|
): Chainable<MatrixClient> {
|
||||||
|
@ -119,7 +119,7 @@ function setupBotClient(
|
||||||
};
|
};
|
||||||
|
|
||||||
const cli = new win.matrixcs.MatrixClient({
|
const cli = new win.matrixcs.MatrixClient({
|
||||||
baseUrl: synapse.baseUrl,
|
baseUrl: homeserver.baseUrl,
|
||||||
userId: credentials.userId,
|
userId: credentials.userId,
|
||||||
deviceId: credentials.deviceId,
|
deviceId: credentials.deviceId,
|
||||||
accessToken: credentials.accessToken,
|
accessToken: credentials.accessToken,
|
||||||
|
@ -160,15 +160,15 @@ function setupBotClient(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts): Chainable<CypressBot> => {
|
Cypress.Commands.add("getBot", (homeserver: HomeserverInstance, opts: CreateBotOpts): Chainable<CypressBot> => {
|
||||||
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||||
const username = Cypress._.uniqueId(opts.userIdPrefix);
|
const username = Cypress._.uniqueId(opts.userIdPrefix);
|
||||||
const password = Cypress._.uniqueId("password_");
|
const password = Cypress._.uniqueId("password_");
|
||||||
return cy
|
return cy
|
||||||
.registerUser(synapse, username, password, opts.displayName)
|
.registerUser(homeserver, username, password, opts.displayName)
|
||||||
.then((credentials) => {
|
.then((credentials) => {
|
||||||
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
cy.log(`Registered bot user ${username} with displayname ${opts.displayName}`);
|
||||||
return setupBotClient(synapse, credentials, opts);
|
return setupBotClient(homeserver, credentials, opts);
|
||||||
})
|
})
|
||||||
.then((client): Chainable<CypressBot> => {
|
.then((client): Chainable<CypressBot> => {
|
||||||
Object.assign(client, { __cypress_password: password });
|
Object.assign(client, { __cypress_password: password });
|
||||||
|
@ -178,10 +178,15 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
||||||
|
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
"loginBot",
|
"loginBot",
|
||||||
(synapse: SynapseInstance, username: string, password: string, opts: CreateBotOpts): Chainable<MatrixClient> => {
|
(
|
||||||
|
homeserver: HomeserverInstance,
|
||||||
|
username: string,
|
||||||
|
password: string,
|
||||||
|
opts: CreateBotOpts,
|
||||||
|
): Chainable<MatrixClient> => {
|
||||||
opts = Object.assign({}, defaultCreateBotOptions, { bootstrapCrossSigning: false }, opts);
|
opts = Object.assign({}, defaultCreateBotOptions, { bootstrapCrossSigning: false }, opts);
|
||||||
return cy.loginUser(synapse, username, password).then((credentials) => {
|
return cy.loginUser(homeserver, username, password).then((credentials) => {
|
||||||
return setupBotClient(synapse, credentials, opts);
|
return setupBotClient(homeserver, credentials, opts);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||||
import "@percy/cypress";
|
import "@percy/cypress";
|
||||||
import "cypress-real-events";
|
import "cypress-real-events";
|
||||||
|
|
||||||
import "./synapse";
|
import "./homeserver";
|
||||||
import "./login";
|
import "./login";
|
||||||
import "./labs";
|
import "./labs";
|
||||||
import "./client";
|
import "./client";
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -20,34 +20,34 @@ import * as crypto from "crypto";
|
||||||
|
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import AUTWindow = Cypress.AUTWindow;
|
import AUTWindow = Cypress.AUTWindow;
|
||||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
import { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
namespace Cypress {
|
namespace Cypress {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
/**
|
/**
|
||||||
* Start a synapse instance with a given config template.
|
* Start a homeserver instance with a given config template.
|
||||||
* @param template path to template within cypress/plugins/synapsedocker/template/ directory.
|
* @param template path to template within cypress/plugins/{homeserver}docker/template/ directory.
|
||||||
*/
|
*/
|
||||||
startSynapse(template: string): Chainable<SynapseInstance>;
|
startHomeserver(template: string): Chainable<HomeserverInstance>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom command wrapping task:synapseStop whilst preventing uncaught exceptions
|
* Custom command wrapping task:{homeserver}Stop whilst preventing uncaught exceptions
|
||||||
* for if Synapse stopping races with the app's background sync loop.
|
* for if Homeserver stopping races with the app's background sync loop.
|
||||||
* @param synapse the synapse instance returned by startSynapse
|
* @param homeserver the homeserver instance returned by start{Homeserver}
|
||||||
*/
|
*/
|
||||||
stopSynapse(synapse: SynapseInstance): Chainable<AUTWindow>;
|
stopHomeserver(homeserver: HomeserverInstance): Chainable<AUTWindow>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a user on the given Synapse using the shared registration secret.
|
* Register a user on the given Homeserver using the shared registration secret.
|
||||||
* @param synapse the synapse instance returned by startSynapse
|
* @param homeserver the homeserver instance returned by start{Homeserver}
|
||||||
* @param username the username of the user to register
|
* @param username the username of the user to register
|
||||||
* @param password the password of the user to register
|
* @param password the password of the user to register
|
||||||
* @param displayName optional display name to set on the newly registered user
|
* @param displayName optional display name to set on the newly registered user
|
||||||
*/
|
*/
|
||||||
registerUser(
|
registerUser(
|
||||||
synapse: SynapseInstance,
|
homeserver: HomeserverInstance,
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
|
@ -56,16 +56,18 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSynapse(template: string): Chainable<SynapseInstance> {
|
function startHomeserver(template: string): Chainable<HomeserverInstance> {
|
||||||
return cy.task<SynapseInstance>("synapseStart", template);
|
const homeserverName = Cypress.env("HOMESERVER");
|
||||||
|
return cy.task<HomeserverInstance>(homeserverName + "Start", template);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopSynapse(synapse?: SynapseInstance): Chainable<AUTWindow> {
|
function stopHomeserver(homeserver?: HomeserverInstance): Chainable<AUTWindow> {
|
||||||
if (!synapse) return;
|
if (!homeserver) return;
|
||||||
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
// Navigate away from app to stop the background network requests which will race with Homeserver shutting down
|
||||||
return cy.window({ log: false }).then((win) => {
|
return cy.window({ log: false }).then((win) => {
|
||||||
win.location.href = "about:blank";
|
win.location.href = "about:blank";
|
||||||
cy.task("synapseStop", synapse.synapseId);
|
const homeserverName = Cypress.env("HOMESERVER");
|
||||||
|
cy.task(homeserverName + "Stop", homeserver.serverId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,12 +79,12 @@ export interface Credentials {
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerUser(
|
function registerUser(
|
||||||
synapse: SynapseInstance,
|
homeserver: HomeserverInstance,
|
||||||
username: string,
|
username: string,
|
||||||
password: string,
|
password: string,
|
||||||
displayName?: string,
|
displayName?: string,
|
||||||
): Chainable<Credentials> {
|
): Chainable<Credentials> {
|
||||||
const url = `${synapse.baseUrl}/_synapse/admin/v1/register`;
|
const url = `${homeserver.baseUrl}/_synapse/admin/v1/register`;
|
||||||
return cy
|
return cy
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// get a nonce
|
// get a nonce
|
||||||
|
@ -91,7 +93,7 @@ function registerUser(
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const { nonce } = response.body;
|
const { nonce } = response.body;
|
||||||
const mac = crypto
|
const mac = crypto
|
||||||
.createHmac("sha1", synapse.registrationSecret)
|
.createHmac("sha1", homeserver.registrationSecret)
|
||||||
.update(`${nonce}\0${username}\0${password}\0notadmin`)
|
.update(`${nonce}\0${username}\0${password}\0notadmin`)
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
|
|
||||||
|
@ -121,6 +123,6 @@ function registerUser(
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
Cypress.Commands.add("startSynapse", startSynapse);
|
Cypress.Commands.add("startHomeserver", startHomeserver);
|
||||||
Cypress.Commands.add("stopSynapse", stopSynapse);
|
Cypress.Commands.add("stopHomeserver", stopHomeserver);
|
||||||
Cypress.Commands.add("registerUser", registerUser);
|
Cypress.Commands.add("registerUser", registerUser);
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
import { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||||
|
|
||||||
export interface UserCredentials {
|
export interface UserCredentials {
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
@ -41,7 +41,7 @@ declare global {
|
||||||
* useed.
|
* useed.
|
||||||
*/
|
*/
|
||||||
initTestUser(
|
initTestUser(
|
||||||
synapse: SynapseInstance,
|
homeserver: HomeserverInstance,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
prelaunchFn?: () => void,
|
prelaunchFn?: () => void,
|
||||||
userIdPrefix?: string,
|
userIdPrefix?: string,
|
||||||
|
@ -52,7 +52,7 @@ declare global {
|
||||||
* @param username login username
|
* @param username login username
|
||||||
* @param password login password
|
* @param password login password
|
||||||
*/
|
*/
|
||||||
loginUser(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials>;
|
loginUser(synapse: HomeserverInstance, username: string, password: string): Chainable<UserCredentials>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -60,8 +60,8 @@ declare global {
|
||||||
// eslint-disable-next-line max-len
|
// eslint-disable-next-line max-len
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
"loginUser",
|
"loginUser",
|
||||||
(synapse: SynapseInstance, username: string, password: string): Chainable<UserCredentials> => {
|
(homeserver: HomeserverInstance, username: string, password: string): Chainable<UserCredentials> => {
|
||||||
const url = `${synapse.baseUrl}/_matrix/client/r0/login`;
|
const url = `${homeserver.baseUrl}/_matrix/client/r0/login`;
|
||||||
return cy
|
return cy
|
||||||
.request<{
|
.request<{
|
||||||
access_token: string;
|
access_token: string;
|
||||||
|
@ -95,7 +95,7 @@ Cypress.Commands.add(
|
||||||
Cypress.Commands.add(
|
Cypress.Commands.add(
|
||||||
"initTestUser",
|
"initTestUser",
|
||||||
(
|
(
|
||||||
synapse: SynapseInstance,
|
homeserver: HomeserverInstance,
|
||||||
displayName: string,
|
displayName: string,
|
||||||
prelaunchFn?: () => void,
|
prelaunchFn?: () => void,
|
||||||
userIdPrefix = "user_",
|
userIdPrefix = "user_",
|
||||||
|
@ -112,15 +112,15 @@ Cypress.Commands.add(
|
||||||
const username = Cypress._.uniqueId(userIdPrefix);
|
const username = Cypress._.uniqueId(userIdPrefix);
|
||||||
const password = Cypress._.uniqueId("password_");
|
const password = Cypress._.uniqueId("password_");
|
||||||
return cy
|
return cy
|
||||||
.registerUser(synapse, username, password, displayName)
|
.registerUser(homeserver, username, password, displayName)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return cy.loginUser(synapse, username, password);
|
return cy.loginUser(homeserver, username, password);
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
cy.log(`Registered test user ${username} with displayname ${displayName}`);
|
||||||
cy.window({ log: false }).then((win) => {
|
cy.window({ log: false }).then((win) => {
|
||||||
// Seed the localStorage with the required credentials
|
// Seed the localStorage with the required credentials
|
||||||
win.localStorage.setItem("mx_hs_url", synapse.baseUrl);
|
win.localStorage.setItem("mx_hs_url", homeserver.baseUrl);
|
||||||
win.localStorage.setItem("mx_user_id", response.userId);
|
win.localStorage.setItem("mx_user_id", response.userId);
|
||||||
win.localStorage.setItem("mx_access_token", response.accessToken);
|
win.localStorage.setItem("mx_access_token", response.accessToken);
|
||||||
win.localStorage.setItem("mx_device_id", response.deviceId);
|
win.localStorage.setItem("mx_device_id", response.deviceId);
|
||||||
|
|
|
@ -19,7 +19,7 @@ limitations under the License.
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
import AUTWindow = Cypress.AUTWindow;
|
import AUTWindow = Cypress.AUTWindow;
|
||||||
import { ProxyInstance } from "../plugins/sliding-sync";
|
import { ProxyInstance } from "../plugins/sliding-sync";
|
||||||
import { SynapseInstance } from "../plugins/synapsedocker";
|
import { HomeserverInstance } from "../plugins/utils/homeserver";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
@ -27,9 +27,9 @@ declare global {
|
||||||
interface Chainable {
|
interface Chainable {
|
||||||
/**
|
/**
|
||||||
* Start a sliding sync proxy instance.
|
* Start a sliding sync proxy instance.
|
||||||
* @param synapse the synapse instance returned by startSynapse
|
* @param homeserver the homeserver instance returned by startHomeserver
|
||||||
*/
|
*/
|
||||||
startProxy(synapse: SynapseInstance): Chainable<ProxyInstance>;
|
startProxy(homeserver: HomeserverInstance): Chainable<ProxyInstance>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom command wrapping task:proxyStop whilst preventing uncaught exceptions
|
* Custom command wrapping task:proxyStop whilst preventing uncaught exceptions
|
||||||
|
@ -41,13 +41,13 @@ declare global {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function startProxy(synapse: SynapseInstance): Chainable<ProxyInstance> {
|
function startProxy(homeserver: HomeserverInstance): Chainable<ProxyInstance> {
|
||||||
return cy.task<ProxyInstance>("proxyStart", synapse);
|
return cy.task<ProxyInstance>("proxyStart", homeserver);
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
|
function stopProxy(proxy?: ProxyInstance): Chainable<AUTWindow> {
|
||||||
if (!proxy) return;
|
if (!proxy) return;
|
||||||
// Navigate away from app to stop the background network requests which will race with Synapse shutting down
|
// Navigate away from app to stop the background network requests which will race with Homeserver shutting down
|
||||||
return cy.window({ log: false }).then((win) => {
|
return cy.window({ log: false }).then((win) => {
|
||||||
win.location.href = "about:blank";
|
win.location.href = "about:blank";
|
||||||
cy.task("proxyStop", proxy);
|
cy.task("proxyStop", proxy);
|
||||||
|
|
|
@ -21,7 +21,7 @@ be tested. When running Cypress tests yourself, the standard `yarn start` from t
|
||||||
element-web project is fine: leave it running it a different terminal as you would
|
element-web project is fine: leave it running it a different terminal as you would
|
||||||
when developing.
|
when developing.
|
||||||
|
|
||||||
The tests use Docker to launch Synapse instances to test against, so you'll also
|
The tests use Docker to launch Homeserver (Synapse or Dendrite) instances to test against, so you'll also
|
||||||
need to have Docker installed and working in order to run the Cypress tests.
|
need to have Docker installed and working in order to run the Cypress tests.
|
||||||
|
|
||||||
There are a few different ways to run the tests yourself. The simplest is to run:
|
There are a few different ways to run the tests yourself. The simplest is to run:
|
||||||
|
@ -58,10 +58,10 @@ Synapse can be launched with different configurations in order to test element
|
||||||
in different configurations. `cypress/plugins/synapsedocker/templates` contains
|
in different configurations. `cypress/plugins/synapsedocker/templates` contains
|
||||||
template configuration files for each different configuration.
|
template configuration files for each different configuration.
|
||||||
|
|
||||||
Each test suite can then launch whatever Synapse instances it needs it whatever
|
Each test suite can then launch whatever Synapse instances it needs in whatever
|
||||||
configurations.
|
configurations.
|
||||||
|
|
||||||
Note that although tests should stop the Synapse instances after running and the
|
Note that although tests should stop the Homeserver instances after running and the
|
||||||
plugin also stop any remaining instances after all tests have run, it is possible
|
plugin also stop any remaining instances after all tests have run, it is possible
|
||||||
to be left with some stray containers if, for example, you terminate a test such
|
to be left with some stray containers if, for example, you terminate a test such
|
||||||
that the `after()` does not run and also exit Cypress uncleanly. All the containers
|
that the `after()` does not run and also exit Cypress uncleanly. All the containers
|
||||||
|
@ -82,29 +82,29 @@ a read.
|
||||||
### Getting a Synapse
|
### Getting a Synapse
|
||||||
|
|
||||||
The key difference is in starting Synapse instances. Tests use this plugin via
|
The key difference is in starting Synapse instances. Tests use this plugin via
|
||||||
`cy.startSynapse()` to provide a Synapse instance to log into:
|
`cy.startHomeserver()` to provide a Homeserver instance to log into:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
cy.startSynapse("consent").then((result) => {
|
cy.startHomeserver("consent").then((result) => {
|
||||||
synapse = result;
|
homeserver = result;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
This returns an object with information about the Synapse instance, including what port
|
This returns an object with information about the Homeserver instance, including what port
|
||||||
it was started on and the ID that needs to be passed to shut it down again. It also
|
it was started on and the ID that needs to be passed to shut it down again. It also
|
||||||
returns the registration shared secret (`registrationSecret`) that can be used to
|
returns the registration shared secret (`registrationSecret`) that can be used to
|
||||||
register users via the REST API. The Synapse has been ensured ready to go by awaiting
|
register users via the REST API. The Homeserver has been ensured ready to go by awaiting
|
||||||
its internal health-check.
|
its internal health-check.
|
||||||
|
|
||||||
Synapse instances should be reasonably cheap to start (you may see the first one take a
|
Homeserver instances should be reasonably cheap to start (you may see the first one take a
|
||||||
while as it pulls the Docker image), so it's generally expected that tests will start a
|
while as it pulls the Docker image), so it's generally expected that tests will start a
|
||||||
Synapse instance for each test suite, i.e. in `before()`, and then tear it down in `after()`.
|
Homeserver instance for each test suite, i.e. in `before()`, and then tear it down in `after()`.
|
||||||
|
|
||||||
To later destroy your Synapse you should call `stopSynapse`, passing the SynapseInstance
|
To later destroy your Homeserver you should call `stopHomeserver`, passing the HomeserverInstance
|
||||||
object you received when starting it.
|
object you received when starting it.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
cy.stopSynapse(synapse);
|
cy.stopHomeserver(homeserver);
|
||||||
```
|
```
|
||||||
|
|
||||||
### Synapse Config Templates
|
### Synapse Config Templates
|
||||||
|
@ -131,10 +131,10 @@ in a template can be referenced in the config as `/data/foo.html`.
|
||||||
There exists a basic utility to start the app with a random user already logged in:
|
There exists a basic utility to start the app with a random user already logged in:
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
cy.initTestUser(synapse, "Jeff");
|
cy.initTestUser(homeserver, "Jeff");
|
||||||
```
|
```
|
||||||
|
|
||||||
It takes the SynapseInstance you received from `startSynapse` and a display name for your test user.
|
It takes the HomeserverInstance you received from `startHomeserver` and a display name for your test user.
|
||||||
This custom command will register a random userId using the registrationSecret with a random password
|
This custom command will register a random userId using the registrationSecret with a random password
|
||||||
and the given display name. The returned Chainable will contain details about the credentials for if
|
and the given display name. The returned Chainable will contain details about the credentials for if
|
||||||
they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
|
they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
|
||||||
|
@ -147,11 +147,11 @@ but the signature can be maintained for simpler maintenance.
|
||||||
|
|
||||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
||||||
way to do this may be to get an access token for the user and use this to create a room with the REST
|
way to do this may be to get an access token for the user and use this to create a room with the REST
|
||||||
API before logging the user in. You can make use of `cy.getBot(synapse)` and `cy.getClient()` to do this.
|
API before logging the user in. You can make use of `cy.getBot(homeserver)` and `cy.getClient()` to do this.
|
||||||
|
|
||||||
### Convenience APIs
|
### Convenience APIs
|
||||||
|
|
||||||
We should probably end up with convenience APIs that wrap the synapse creation, logging in and room
|
We should probably end up with convenience APIs that wrap the homeserver creation, logging in and room
|
||||||
creation that can be called to set up tests.
|
creation that can be called to set up tests.
|
||||||
|
|
||||||
### Using matrix-js-sdk
|
### Using matrix-js-sdk
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
"@matrix-org/analytics-events": "^0.3.0",
|
"@matrix-org/analytics-events": "^0.3.0",
|
||||||
"@matrix-org/matrix-wysiwyg": "^0.13.0",
|
"@matrix-org/matrix-wysiwyg": "^0.14.0",
|
||||||
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
"@matrix-org/react-sdk-module-api": "^0.0.3",
|
||||||
"@sentry/browser": "^7.0.0",
|
"@sentry/browser": "^7.0.0",
|
||||||
"@sentry/tracing": "^7.0.0",
|
"@sentry/tracing": "^7.0.0",
|
||||||
|
|
|
@ -27,6 +27,11 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_CompoundDialog {
|
.mx_CompoundDialog {
|
||||||
|
.mx_Dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CompoundDialog_header {
|
.mx_CompoundDialog_header {
|
||||||
padding: 32px 32px 16px 32px;
|
padding: 32px 32px 16px 32px;
|
||||||
|
|
||||||
|
@ -49,6 +54,13 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CompoundDialog_form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_CompoundDialog_content {
|
.mx_CompoundDialog_content {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 8px 32px;
|
padding: 8px 32px;
|
||||||
|
@ -57,10 +69,6 @@ limitations under the License.
|
||||||
.mx_CompoundDialog_footer {
|
.mx_CompoundDialog_footer {
|
||||||
padding: 20px 32px;
|
padding: 20px 32px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
|
@ -69,14 +77,17 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ScrollableBaseDialog {
|
.mx_ScrollableBaseDialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
width: 544px; /* fixed */
|
width: 544px; /* fixed */
|
||||||
height: 516px; /* fixed */
|
height: 516px; /* fixed */
|
||||||
|
max-width: 100%;
|
||||||
.mx_CompoundDialog_content {
|
min-height: 0;
|
||||||
height: 349px; /* dialogHeight - header - footer */
|
max-height: 80%;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CompoundDialog_footer {
|
.mx_CompoundDialog_footer {
|
||||||
box-shadow: 0px -4px 4px rgba(0, 0, 0, 0.05); /* hardcoded colour for both themes */
|
box-shadow: 0px -4px 4px rgba(0, 0, 0, 0.05); /* hardcoded colour for both themes */
|
||||||
|
z-index: 1; /* needed to make footer & shadow appear above dialog content */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,14 +16,32 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_LinkModal {
|
.mx_LinkModal {
|
||||||
padding: $spacing-32;
|
padding: $spacing-32;
|
||||||
|
max-width: 600px;
|
||||||
.mx_Dialog_content {
|
height: 341px;
|
||||||
margin-top: 30px;
|
box-sizing: border-box;
|
||||||
margin-bottom: 42px;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
|
||||||
.mx_LinkModal_content {
|
.mx_LinkModal_content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
gap: $spacing-8;
|
||||||
|
margin-top: 7px;
|
||||||
|
|
||||||
|
.mx_LinkModal_Field {
|
||||||
|
flex: initial;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LinkModal_buttons {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
align-items: flex-end;
|
||||||
|
|
||||||
|
.mx_Dialog_buttons {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,31 +4,14 @@
|
||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
>
|
<g clip-path="url(#clip0_2168_154906)">
|
||||||
<g
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5467 15.8667L13.6667 13.7333H7.00005L6.13338 15.8667C5.93338 16.3467 5.46672 16.6667 4.94672 16.6667C4.02672 16.6667 3.40005 15.72 3.76005 14.88L5.00323 12.0059H15.6635L16.9067 14.88C17.2667 15.72 16.6534 16.6667 15.7334 16.6667C15.2134 16.6667 14.7467 16.3467 14.5467 15.8667ZM13.6435 7.33594L11.7334 2.92C11.4934 2.36 10.9467 2 10.3334 2C9.72005 2 9.17338 2.36 8.93338 2.92L7.02326 7.33594H9.32912L10.3334 4.64L11.3376 7.33594H13.6435Z" fill="currentColor"/>
|
||||||
clip-path="url(#clip0_1456_146365)"
|
<path d="M1 9.67708C1 10.4104 1.6 11.0104 2.33333 11.0104H18.3333C19.0667 11.0104 19.6667 10.4104 19.6667 9.67708C19.6667 8.94375 19.0667 8.34375 18.3333 8.34375H2.33333C1.6 8.34375 1 8.94375 1 9.67708Z" fill="currentColor"/>
|
||||||
id="g53">
|
|
||||||
<path
|
|
||||||
d="M7.00042 13.7333H13.6671L14.5471 15.8667C14.7471 16.3467 15.2137 16.6667 15.7337 16.6667C16.6537 16.6667 17.2671 15.72 16.9071 14.88L11.7337 2.92C11.4937 2.36 10.9471 2 10.3337 2C9.72042 2 9.17375 2.36 8.93375 2.92L3.76042 14.88C3.40042 15.72 4.02708 16.6667 4.94708 16.6667C5.46708 16.6667 5.93375 16.3467 6.13375 15.8667L7.00042 13.7333ZM10.3337 4.64L12.8271 11.3333H7.84042L10.3337 4.64Z"
|
|
||||||
fill="#C1C6CD"
|
|
||||||
id="path49" />
|
|
||||||
<path
|
|
||||||
d="m 1.497495,8.96927 c 0,0.793654 0.7402877,1.441437 1.6473569,1.441437 H 17.521786 c 0.907096,0 1.647419,-0.647783 1.647419,-1.441437 0,-0.7936857 -0.740323,-1.4414375 -1.647419,-1.4414375 H 11.127487 3.1448519 c -0.4734211,0 -0.9014103,0.1764504 -1.2024293,0.4580061 C 1.7722258,8.1450309 1.6426187,8.3378225 1.568339,8.5513189 1.522281,8.6837006 1.497495,8.8240421 1.497495,8.96927 Z"
|
|
||||||
fill="#c1c6cd"
|
|
||||||
stroke="#ffffff"
|
|
||||||
id="path51"
|
|
||||||
style="stroke:none;stroke-width:0.840525;stroke-opacity:1" />
|
|
||||||
</g>
|
</g>
|
||||||
<defs
|
<defs>
|
||||||
id="defs58">
|
<clipPath id="clip0_2168_154906">
|
||||||
<clipPath
|
<rect width="20" height="20" fill="white"/>
|
||||||
id="clip0_1456_146365">
|
|
||||||
<rect
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
fill="white"
|
|
||||||
id="rect55" />
|
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.1 KiB |
|
@ -20,15 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
import { removeDirectionOverrideChars } from "matrix-js-sdk/src/utils";
|
||||||
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
import { GuestAccess, HistoryVisibility, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import {
|
import { M_POLL_START, M_POLL_END, PollStartEvent } from "matrix-events-sdk";
|
||||||
M_EMOTE,
|
|
||||||
M_NOTICE,
|
|
||||||
M_MESSAGE,
|
|
||||||
MessageEvent,
|
|
||||||
M_POLL_START,
|
|
||||||
M_POLL_END,
|
|
||||||
PollStartEvent,
|
|
||||||
} from "matrix-events-sdk";
|
|
||||||
|
|
||||||
import { _t } from "./languageHandler";
|
import { _t } from "./languageHandler";
|
||||||
import * as Roles from "./Roles";
|
import * as Roles from "./Roles";
|
||||||
|
@ -347,17 +339,6 @@ function textForMessageEvent(ev: MatrixEvent): () => string | null {
|
||||||
message = textForRedactedPollAndMessageEvent(ev);
|
message = textForRedactedPollAndMessageEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (SettingsStore.isEnabled("feature_extensible_events")) {
|
|
||||||
const extev = ev.unstableExtensibleEvent as MessageEvent;
|
|
||||||
if (extev) {
|
|
||||||
if (extev.isEquivalentTo(M_EMOTE)) {
|
|
||||||
return `* ${senderDisplayName} ${extev.text}`;
|
|
||||||
} else if (extev.isEquivalentTo(M_NOTICE) || extev.isEquivalentTo(M_MESSAGE)) {
|
|
||||||
return `${senderDisplayName}: ${extev.text}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ev.getContent().msgtype === MsgType.Emote) {
|
if (ev.getContent().msgtype === MsgType.Emote) {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
} else if (ev.getContent().msgtype === MsgType.Image) {
|
} else if (ev.getContent().msgtype === MsgType.Image) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
import { M_BEACON } from "matrix-js-sdk/src/@types/beacon";
|
||||||
|
@ -59,35 +60,39 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const timeline of [room, ...room.getThreads()]) {
|
||||||
|
// If the current timeline has unread messages, we're done.
|
||||||
|
if (doesRoomOrThreadHaveUnreadMessages(timeline)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we got here then no timelines were found with unread messages.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doesRoomOrThreadHaveUnreadMessages(roomOrThread: Room | Thread): boolean {
|
||||||
|
// If there are no messages yet in the timeline then it isn't fully initialised
|
||||||
|
// and cannot be unread.
|
||||||
|
if (!roomOrThread || roomOrThread.timeline.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
// get the most recent read receipt sent by our account.
|
|
||||||
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
|
||||||
// despite the name of the method :((
|
|
||||||
const readUpToId = room.getEventReadUpTo(myUserId!);
|
|
||||||
|
|
||||||
if (!SettingsStore.getValue("feature_threadenabled")) {
|
|
||||||
// as we don't send RRs for our own messages, make sure we special case that
|
// as we don't send RRs for our own messages, make sure we special case that
|
||||||
// if *we* sent the last message into the room, we consider it not unread!
|
// if *we* sent the last message into the room, we consider it not unread!
|
||||||
// Should fix: https://github.com/vector-im/element-web/issues/3263
|
// Should fix: https://github.com/vector-im/element-web/issues/3263
|
||||||
// https://github.com/vector-im/element-web/issues/2427
|
// https://github.com/vector-im/element-web/issues/2427
|
||||||
// ...and possibly some of the others at
|
// ...and possibly some of the others at
|
||||||
// https://github.com/vector-im/element-web/issues/3363
|
// https://github.com/vector-im/element-web/issues/3363
|
||||||
if (room.timeline.length && room.timeline[room.timeline.length - 1].getSender() === myUserId) {
|
if (roomOrThread.timeline.at(-1)?.getSender() === myUserId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if the read receipt relates to an event is that part of a thread
|
// get the most recent read receipt sent by our account.
|
||||||
// we consider that there are no unread messages
|
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
|
||||||
// This might be a false negative, but probably the best we can do until
|
// despite the name of the method :((
|
||||||
// the read receipts have evolved to cater for threads
|
const readUpToId = roomOrThread.getEventReadUpTo(myUserId!);
|
||||||
if (readUpToId) {
|
|
||||||
const event = room.findEventById(readUpToId);
|
|
||||||
if (event?.getThread()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this just looks at whatever history we have, which if we've only just started
|
// this just looks at whatever history we have, which if we've only just started
|
||||||
// up probably won't be very much, so if the last couple of events are ones that
|
// up probably won't be very much, so if the last couple of events are ones that
|
||||||
|
@ -96,8 +101,8 @@ export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
// but currently we just guess.
|
// but currently we just guess.
|
||||||
|
|
||||||
// Loop through messages, starting with the most recent...
|
// Loop through messages, starting with the most recent...
|
||||||
for (let i = room.timeline.length - 1; i >= 0; --i) {
|
for (let i = roomOrThread.timeline.length - 1; i >= 0; --i) {
|
||||||
const ev = room.timeline[i];
|
const ev = roomOrThread.timeline[i];
|
||||||
|
|
||||||
if (ev.getId() == readUpToId) {
|
if (ev.getId() == readUpToId) {
|
||||||
// If we've read up to this event, there's nothing more recent
|
// If we've read up to this event, there's nothing more recent
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { _t } from "../../../languageHandler";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import InfoDialog from "./InfoDialog";
|
||||||
|
|
||||||
|
export const createCantStartVoiceMessageBroadcastDialog = (): void => {
|
||||||
|
Modal.createDialog(InfoDialog, {
|
||||||
|
title: _t("Can't start voice message"),
|
||||||
|
description: (
|
||||||
|
<p>
|
||||||
|
{_t(
|
||||||
|
"You can't start a voice message as you are currently recording a live broadcast. " +
|
||||||
|
"Please end your live broadcast in order to start recording a voice message.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
hasCloseButton: true,
|
||||||
|
});
|
||||||
|
};
|
|
@ -96,7 +96,7 @@ export default abstract class ScrollableBaseModal<
|
||||||
aria-label={_t("Close dialog")}
|
aria-label={_t("Close dialog")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={this.onSubmit}>
|
<form onSubmit={this.onSubmit} className="mx_CompoundDialog_form">
|
||||||
<div className="mx_CompoundDialog_content">{this.renderContent()}</div>
|
<div className="mx_CompoundDialog_content">{this.renderContent()}</div>
|
||||||
<div className="mx_CompoundDialog_footer">
|
<div className="mx_CompoundDialog_footer">
|
||||||
<AccessibleButton onClick={this.onCancel} kind="primary_outline">
|
<AccessibleButton onClick={this.onCancel} kind="primary_outline">
|
||||||
|
|
|
@ -262,7 +262,7 @@ export default class Field extends React.PureComponent<PropShapes, IState> {
|
||||||
|
|
||||||
this.inputRef = inputRef || React.createRef();
|
this.inputRef = inputRef || React.createRef();
|
||||||
|
|
||||||
inputProps.placeholder = inputProps.placeholder || inputProps.label;
|
inputProps.placeholder = inputProps.placeholder ?? inputProps.label;
|
||||||
inputProps.id = this.id; // this overwrites the id from props
|
inputProps.id = this.id; // this overwrites the id from props
|
||||||
|
|
||||||
inputProps.onFocus = this.onFocus;
|
inputProps.onFocus = this.onFocus;
|
||||||
|
|
|
@ -18,7 +18,6 @@ import React, { createRef, SyntheticEvent, MouseEvent, ReactNode } from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import highlight from "highlight.js";
|
import highlight from "highlight.js";
|
||||||
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { isEventLike, LegacyMsgType, M_MESSAGE, MessageEvent } from "matrix-events-sdk";
|
|
||||||
|
|
||||||
import * as HtmlUtils from "../../../HtmlUtils";
|
import * as HtmlUtils from "../../../HtmlUtils";
|
||||||
import { formatDate } from "../../../DateUtils";
|
import { formatDate } from "../../../DateUtils";
|
||||||
|
@ -579,29 +578,6 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
|
||||||
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
|
// only strip reply if this is the original replying event, edits thereafter do not have the fallback
|
||||||
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
|
const stripReply = !mxEvent.replacingEvent() && !!getParentEventId(mxEvent);
|
||||||
let body: ReactNode;
|
let body: ReactNode;
|
||||||
if (SettingsStore.isEnabled("feature_extensible_events")) {
|
|
||||||
const extev = this.props.mxEvent.unstableExtensibleEvent as MessageEvent;
|
|
||||||
if (extev?.isEquivalentTo(M_MESSAGE)) {
|
|
||||||
isEmote = isEventLike(extev.wireFormat, LegacyMsgType.Emote);
|
|
||||||
isNotice = isEventLike(extev.wireFormat, LegacyMsgType.Notice);
|
|
||||||
body = HtmlUtils.bodyToHtml(
|
|
||||||
{
|
|
||||||
body: extev.text,
|
|
||||||
format: extev.html ? "org.matrix.custom.html" : undefined,
|
|
||||||
formatted_body: extev.html,
|
|
||||||
msgtype: MsgType.Text,
|
|
||||||
},
|
|
||||||
this.props.highlights,
|
|
||||||
{
|
|
||||||
disableBigEmoji: isEmote || !SettingsStore.getValue<boolean>("TextualBody.enableBigEmoji"),
|
|
||||||
// Part of Replies fallback support
|
|
||||||
stripReplyFallback: stripReply,
|
|
||||||
ref: this.contentRef,
|
|
||||||
returnString: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!body) {
|
if (!body) {
|
||||||
isEmote = content.msgtype === MsgType.Emote;
|
isEmote = content.msgtype === MsgType.Emote;
|
||||||
isNotice = content.msgtype === MsgType.Notice;
|
isNotice = content.msgtype === MsgType.Notice;
|
||||||
|
|
|
@ -69,7 +69,7 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
|
||||||
if (onClose) {
|
if (onClose) {
|
||||||
closeButton = (
|
closeButton = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
data-test-id="base-card-close-button"
|
data-testid="base-card-close-button"
|
||||||
className="mx_BaseCard_close"
|
className="mx_BaseCard_close"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
title={closeLabel || _t("Close")}
|
title={closeLabel || _t("Close")}
|
||||||
|
|
|
@ -21,6 +21,7 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { ThreadEvent } from "matrix-js-sdk/src/models/thread";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
|
@ -44,6 +45,7 @@ import { NotificationStateEvents } from "../../../stores/notifications/Notificat
|
||||||
import PosthogTrackers from "../../../PosthogTrackers";
|
import PosthogTrackers from "../../../PosthogTrackers";
|
||||||
import { ButtonEvent } from "../elements/AccessibleButton";
|
import { ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
import { doesRoomOrThreadHaveUnreadMessages } from "../../../Unread";
|
||||||
|
|
||||||
const ROOM_INFO_PHASES = [
|
const ROOM_INFO_PHASES = [
|
||||||
RightPanelPhases.RoomSummary,
|
RightPanelPhases.RoomSummary,
|
||||||
|
@ -154,7 +156,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
if (!this.supportsThreadNotifications) {
|
if (!this.supportsThreadNotifications) {
|
||||||
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
this.threadNotificationState?.on(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
} else {
|
} else {
|
||||||
|
// Notification badge may change if the notification counts from the
|
||||||
|
// server change, if a new thread is created or updated, or if a
|
||||||
|
// receipt is sent in the thread.
|
||||||
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
this.props.room?.on(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(RoomEvent.Receipt, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(RoomEvent.Timeline, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(RoomEvent.Redaction, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(RoomEvent.MyMembership, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(ThreadEvent.New, this.onNotificationUpdate);
|
||||||
|
this.props.room?.on(ThreadEvent.Update, this.onNotificationUpdate);
|
||||||
}
|
}
|
||||||
this.onNotificationUpdate();
|
this.onNotificationUpdate();
|
||||||
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
RoomNotificationStateStore.instance.on(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||||
|
@ -166,6 +178,13 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
this.threadNotificationState?.off(NotificationStateEvents.Update, this.onNotificationUpdate);
|
||||||
} else {
|
} else {
|
||||||
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
this.props.room?.off(RoomEvent.UnreadNotifications, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(RoomEvent.Receipt, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(RoomEvent.Timeline, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(RoomEvent.Redaction, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(RoomEvent.LocalEchoUpdated, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(RoomEvent.MyMembership, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(ThreadEvent.New, this.onNotificationUpdate);
|
||||||
|
this.props.room?.off(ThreadEvent.Update, this.onNotificationUpdate);
|
||||||
}
|
}
|
||||||
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
RoomNotificationStateStore.instance.off(UPDATE_STATUS_INDICATOR, this.onUpdateStatus);
|
||||||
}
|
}
|
||||||
|
@ -191,9 +210,17 @@ export default class RoomHeaderButtons extends HeaderButtons<IProps> {
|
||||||
return NotificationColor.Red;
|
return NotificationColor.Red;
|
||||||
case NotificationCountType.Total:
|
case NotificationCountType.Total:
|
||||||
return NotificationColor.Grey;
|
return NotificationColor.Grey;
|
||||||
default:
|
|
||||||
return NotificationColor.None;
|
|
||||||
}
|
}
|
||||||
|
// We don't have any notified messages, but we might have unread messages. Let's
|
||||||
|
// find out.
|
||||||
|
for (const thread of this.props.room!.getThreads()) {
|
||||||
|
// If the current thread has unread messages, we're done.
|
||||||
|
if (doesRoomOrThreadHaveUnreadMessages(thread)) {
|
||||||
|
return NotificationColor.Bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, no notification color.
|
||||||
|
return NotificationColor.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {
|
private onUpdateStatus = (notificationState: SummarizedNotificationState): void => {
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||||
|
import { UserTrustLevel } from "matrix-js-sdk/src/crypto/CrossSigning";
|
||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
|
@ -84,7 +85,7 @@ export interface IDevice {
|
||||||
getDisplayName(): string;
|
getDisplayName(): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const disambiguateDevices = (devices: IDevice[]) => {
|
export const disambiguateDevices = (devices: IDevice[]) => {
|
||||||
const names = Object.create(null);
|
const names = Object.create(null);
|
||||||
for (let i = 0; i < devices.length; i++) {
|
for (let i = 0; i < devices.length; i++) {
|
||||||
const name = devices[i].getDisplayName();
|
const name = devices[i].getDisplayName();
|
||||||
|
@ -94,7 +95,7 @@ const disambiguateDevices = (devices: IDevice[]) => {
|
||||||
}
|
}
|
||||||
for (const name in names) {
|
for (const name in names) {
|
||||||
if (names[name].length > 1) {
|
if (names[name].length > 1) {
|
||||||
names[name].forEach((j) => {
|
names[name].forEach((j: number) => {
|
||||||
devices[j].ambiguous = true;
|
devices[j].ambiguous = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -149,7 +150,7 @@ function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: bool
|
||||||
}, [cli, member, canVerify]);
|
}, [cli, member, canVerify]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
|
export function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const isMe = userId === cli.getUserId();
|
const isMe = userId === cli.getUserId();
|
||||||
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
|
const deviceTrust = cli.checkDeviceTrust(userId, device.deviceId);
|
||||||
|
@ -172,7 +173,10 @@ function DeviceItem({ userId, device }: { userId: string; device: IDevice }) {
|
||||||
});
|
});
|
||||||
|
|
||||||
const onDeviceClick = () => {
|
const onDeviceClick = () => {
|
||||||
verifyDevice(cli.getUser(userId), device);
|
const user = cli.getUser(userId);
|
||||||
|
if (user) {
|
||||||
|
verifyDevice(user, device);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let deviceName;
|
let deviceName;
|
||||||
|
@ -315,7 +319,7 @@ const MessageButton = ({ member }: { member: RoomMember }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UserOptionsSection: React.FC<{
|
export const UserOptionsSection: React.FC<{
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
isIgnored: boolean;
|
isIgnored: boolean;
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
|
@ -367,7 +371,8 @@ const UserOptionsSection: React.FC<{
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
event_id: room.getEventReadUpTo(member.userId),
|
// this could return null, the default prevents a type error
|
||||||
|
event_id: room?.getEventReadUpTo(member.userId) || undefined,
|
||||||
room_id: member.roomId,
|
room_id: member.roomId,
|
||||||
metricsTrigger: undefined, // room doesn't change
|
metricsTrigger: undefined, // room doesn't change
|
||||||
});
|
});
|
||||||
|
@ -402,16 +407,18 @@ const UserOptionsSection: React.FC<{
|
||||||
const onInviteUserButton = async (ev: ButtonEvent) => {
|
const onInviteUserButton = async (ev: ButtonEvent) => {
|
||||||
try {
|
try {
|
||||||
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
// We use a MultiInviter to re-use the invite logic, even though we're only inviting one user.
|
||||||
const inviter = new MultiInviter(roomId);
|
const inviter = new MultiInviter(roomId || "");
|
||||||
await inviter.invite([member.userId]).then(() => {
|
await inviter.invite([member.userId]).then(() => {
|
||||||
if (inviter.getCompletionState(member.userId) !== "invited") {
|
if (inviter.getCompletionState(member.userId) !== "invited") {
|
||||||
throw new Error(inviter.getErrorText(member.userId));
|
throw new Error(inviter.getErrorText(member.userId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
const description = err instanceof Error ? err.message : _t("Operation failed");
|
||||||
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Failed to invite"),
|
title: _t("Failed to invite"),
|
||||||
description: err && err.message ? err.message : _t("Operation failed"),
|
description,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,10 +439,7 @@ const UserOptionsSection: React.FC<{
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
let directMessageButton: JSX.Element;
|
const directMessageButton = isMe ? null : <MessageButton member={member} />;
|
||||||
if (!isMe) {
|
|
||||||
directMessageButton = <MessageButton member={member} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo_container">
|
<div className="mx_UserInfo_container">
|
||||||
|
@ -499,16 +503,24 @@ interface IPowerLevelsContent {
|
||||||
redact?: number;
|
redact?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
|
export const isMuted = (member: RoomMember, powerLevelContent: IPowerLevelsContent) => {
|
||||||
if (!powerLevelContent || !member) return false;
|
if (!powerLevelContent || !member) return false;
|
||||||
|
|
||||||
const levelToSend =
|
const levelToSend =
|
||||||
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
|
(powerLevelContent.events ? powerLevelContent.events["m.room.message"] : null) ||
|
||||||
powerLevelContent.events_default;
|
powerLevelContent.events_default;
|
||||||
|
|
||||||
|
// levelToSend could be undefined as .events_default is optional. Coercing in this case using
|
||||||
|
// Number() would always return false, so this preserves behaviour
|
||||||
|
// FIXME: per the spec, if `events_default` is unset, it defaults to zero. If
|
||||||
|
// the member has a negative powerlevel, this will give an incorrect result.
|
||||||
|
if (levelToSend === undefined) return false;
|
||||||
|
|
||||||
return member.powerLevel < levelToSend;
|
return member.powerLevel < levelToSend;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPowerLevels = (room) => room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
export const getPowerLevels = (room: Room): IPowerLevelsContent =>
|
||||||
|
room?.currentState?.getStateEvents(EventType.RoomPowerLevels, "")?.getContent() || {};
|
||||||
|
|
||||||
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
export const useRoomPowerLevels = (cli: MatrixClient, room: Room) => {
|
||||||
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
const [powerLevels, setPowerLevels] = useState<IPowerLevelsContent>(getPowerLevels(room));
|
||||||
|
@ -538,7 +550,7 @@ interface IBaseProps {
|
||||||
stopUpdating(): void;
|
stopUpdating(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
export const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
// check if user can be kicked/disinvited
|
// check if user can be kicked/disinvited
|
||||||
|
@ -566,7 +578,7 @@ const RoomKickButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBas
|
||||||
space: room,
|
space: room,
|
||||||
spaceChildFilter: (child: Room) => {
|
spaceChildFilter: (child: Room) => {
|
||||||
// Return true if the target member is not banned and we have sufficient PL to ban them
|
// Return true if the target member is not banned and we have sufficient PL to ban them
|
||||||
const myMember = child.getMember(cli.credentials.userId);
|
const myMember = child.getMember(cli.credentials.userId || "");
|
||||||
const theirMember = child.getMember(member.userId);
|
const theirMember = child.getMember(member.userId);
|
||||||
return (
|
return (
|
||||||
myMember &&
|
myMember &&
|
||||||
|
@ -648,7 +660,7 @@ const RedactMessagesButton: React.FC<IBaseProps> = ({ member }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
export const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBaseRoomProps, "powerLevels">) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const isBanned = member.membership === "ban";
|
const isBanned = member.membership === "ban";
|
||||||
|
@ -674,7 +686,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBa
|
||||||
spaceChildFilter: isBanned
|
spaceChildFilter: isBanned
|
||||||
? (child: Room) => {
|
? (child: Room) => {
|
||||||
// Return true if the target member is banned and we have sufficient PL to unban
|
// Return true if the target member is banned and we have sufficient PL to unban
|
||||||
const myMember = child.getMember(cli.credentials.userId);
|
const myMember = child.getMember(cli.credentials.userId || "");
|
||||||
const theirMember = child.getMember(member.userId);
|
const theirMember = child.getMember(member.userId);
|
||||||
return (
|
return (
|
||||||
myMember &&
|
myMember &&
|
||||||
|
@ -686,7 +698,7 @@ const BanToggleButton = ({ room, member, startUpdating, stopUpdating }: Omit<IBa
|
||||||
}
|
}
|
||||||
: (child: Room) => {
|
: (child: Room) => {
|
||||||
// Return true if the target member isn't banned and we have sufficient PL to ban
|
// Return true if the target member isn't banned and we have sufficient PL to ban
|
||||||
const myMember = child.getMember(cli.credentials.userId);
|
const myMember = child.getMember(cli.credentials.userId || "");
|
||||||
const theirMember = child.getMember(member.userId);
|
const theirMember = child.getMember(member.userId);
|
||||||
return (
|
return (
|
||||||
myMember &&
|
myMember &&
|
||||||
|
@ -835,7 +847,7 @@ const MuteToggleButton: React.FC<IBaseRoomProps> = ({ member, room, powerLevels,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
export const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
room,
|
room,
|
||||||
children,
|
children,
|
||||||
member,
|
member,
|
||||||
|
@ -855,7 +867,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
// if these do not exist in the event then they should default to 50 as per the spec
|
// if these do not exist in the event then they should default to 50 as per the spec
|
||||||
const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
|
const { ban: banPowerLevel = 50, kick: kickPowerLevel = 50, redact: redactPowerLevel = 50 } = powerLevels;
|
||||||
|
|
||||||
const me = room.getMember(cli.getUserId());
|
const me = room.getMember(cli.getUserId() || "");
|
||||||
if (!me) {
|
if (!me) {
|
||||||
// we aren't in the room, so return no admin tooling
|
// we aren't in the room, so return no admin tooling
|
||||||
return <div />;
|
return <div />;
|
||||||
|
@ -879,7 +891,7 @@ const RoomAdminToolsContainer: React.FC<IBaseRoomProps> = ({
|
||||||
<BanToggleButton room={room} member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
<BanToggleButton room={room} member={member} startUpdating={startUpdating} stopUpdating={stopUpdating} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!isMe && canAffectUser && me.powerLevel >= editPowerLevel && !room.isSpaceRoom()) {
|
if (!isMe && canAffectUser && me.powerLevel >= Number(editPowerLevel) && !room.isSpaceRoom()) {
|
||||||
muteButton = (
|
muteButton = (
|
||||||
<MuteToggleButton
|
<MuteToggleButton
|
||||||
member={member}
|
member={member}
|
||||||
|
@ -949,7 +961,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IR
|
||||||
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
const powerLevels = room?.currentState.getStateEvents(EventType.RoomPowerLevels, "")?.getContent();
|
||||||
if (!powerLevels) return;
|
if (!powerLevels) return;
|
||||||
|
|
||||||
const me = room.getMember(cli.getUserId());
|
const me = room.getMember(cli.getUserId() || "");
|
||||||
if (!me) return;
|
if (!me) return;
|
||||||
|
|
||||||
const them = user;
|
const them = user;
|
||||||
|
@ -1006,7 +1018,7 @@ const PowerLevelSection: React.FC<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const PowerLevelEditor: React.FC<{
|
export const PowerLevelEditor: React.FC<{
|
||||||
user: RoomMember;
|
user: RoomMember;
|
||||||
room: Room;
|
room: Room;
|
||||||
roomPermissions: IRoomPermissions;
|
roomPermissions: IRoomPermissions;
|
||||||
|
@ -1022,8 +1034,13 @@ const PowerLevelEditor: React.FC<{
|
||||||
async (powerLevel: number) => {
|
async (powerLevel: number) => {
|
||||||
setSelectedPowerLevel(powerLevel);
|
setSelectedPowerLevel(powerLevel);
|
||||||
|
|
||||||
const applyPowerChange = (roomId, target, powerLevel, powerLevelEvent) => {
|
const applyPowerChange = (
|
||||||
return cli.setPowerLevel(roomId, target, parseInt(powerLevel), powerLevelEvent).then(
|
roomId: string,
|
||||||
|
target: string,
|
||||||
|
powerLevel: number,
|
||||||
|
powerLevelEvent: MatrixEvent,
|
||||||
|
) => {
|
||||||
|
return cli.setPowerLevel(roomId, target, powerLevel, powerLevelEvent).then(
|
||||||
function () {
|
function () {
|
||||||
// NO-OP; rely on the m.room.member event coming down else we could
|
// NO-OP; rely on the m.room.member event coming down else we could
|
||||||
// get out of sync if we force setState here!
|
// get out of sync if we force setState here!
|
||||||
|
@ -1046,7 +1063,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
if (!powerLevelEvent) return;
|
if (!powerLevelEvent) return;
|
||||||
|
|
||||||
const myUserId = cli.getUserId();
|
const myUserId = cli.getUserId();
|
||||||
const myPower = powerLevelEvent.getContent().users[myUserId];
|
const myPower = powerLevelEvent.getContent().users[myUserId || ""];
|
||||||
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
|
if (myPower && parseInt(myPower) <= powerLevel && myUserId !== target) {
|
||||||
const { finished } = Modal.createDialog(QuestionDialog, {
|
const { finished } = Modal.createDialog(QuestionDialog, {
|
||||||
title: _t("Warning!"),
|
title: _t("Warning!"),
|
||||||
|
@ -1085,7 +1102,7 @@ const PowerLevelEditor: React.FC<{
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo_profileField">
|
<div className="mx_UserInfo_profileField">
|
||||||
<PowerSelector
|
<PowerSelector
|
||||||
label={null}
|
label={undefined}
|
||||||
value={selectedPowerLevel}
|
value={selectedPowerLevel}
|
||||||
maxValue={roomPermissions.modifyLevelMax}
|
maxValue={roomPermissions.modifyLevelMax}
|
||||||
usersDefault={powerLevelUsersDefault}
|
usersDefault={powerLevelUsersDefault}
|
||||||
|
@ -1099,7 +1116,7 @@ export const useDevices = (userId: string) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
// undefined means yet to be loaded, null means failed to load, otherwise list of devices
|
||||||
const [devices, setDevices] = useState(undefined);
|
const [devices, setDevices] = useState<undefined | null | IDevice[]>(undefined);
|
||||||
// Download device lists
|
// Download device lists
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDevices(undefined);
|
setDevices(undefined);
|
||||||
|
@ -1116,8 +1133,8 @@ export const useDevices = (userId: string) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
disambiguateDevices(devices);
|
disambiguateDevices(devices as IDevice[]);
|
||||||
setDevices(devices);
|
setDevices(devices as IDevice[]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setDevices(null);
|
setDevices(null);
|
||||||
}
|
}
|
||||||
|
@ -1136,17 +1153,17 @@ export const useDevices = (userId: string) => {
|
||||||
const updateDevices = async () => {
|
const updateDevices = async () => {
|
||||||
const newDevices = cli.getStoredDevicesForUser(userId);
|
const newDevices = cli.getStoredDevicesForUser(userId);
|
||||||
if (cancel) return;
|
if (cancel) return;
|
||||||
setDevices(newDevices);
|
setDevices(newDevices as IDevice[]);
|
||||||
};
|
};
|
||||||
const onDevicesUpdated = (users) => {
|
const onDevicesUpdated = (users: string[]) => {
|
||||||
if (!users.includes(userId)) return;
|
if (!users.includes(userId)) return;
|
||||||
updateDevices();
|
updateDevices();
|
||||||
};
|
};
|
||||||
const onDeviceVerificationChanged = (_userId, device) => {
|
const onDeviceVerificationChanged = (_userId: string, deviceId: string) => {
|
||||||
if (_userId !== userId) return;
|
if (_userId !== userId) return;
|
||||||
updateDevices();
|
updateDevices();
|
||||||
};
|
};
|
||||||
const onUserTrustStatusChanged = (_userId, trustStatus) => {
|
const onUserTrustStatusChanged = (_userId: string, trustLevel: UserTrustLevel) => {
|
||||||
if (_userId !== userId) return;
|
if (_userId !== userId) return;
|
||||||
updateDevices();
|
updateDevices();
|
||||||
};
|
};
|
||||||
|
@ -1229,9 +1246,11 @@ const BasicUserInfo: React.FC<{
|
||||||
logger.error("Failed to deactivate user");
|
logger.error("Failed to deactivate user");
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
|
|
||||||
|
const description = err instanceof Error ? err.message : _t("Operation failed");
|
||||||
|
|
||||||
Modal.createDialog(ErrorDialog, {
|
Modal.createDialog(ErrorDialog, {
|
||||||
title: _t("Failed to deactivate user"),
|
title: _t("Failed to deactivate user"),
|
||||||
description: err && err.message ? err.message : _t("Operation failed"),
|
description,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [cli, member.userId]);
|
}, [cli, member.userId]);
|
||||||
|
@ -1317,12 +1336,12 @@ const BasicUserInfo: React.FC<{
|
||||||
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
|
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
|
||||||
|
|
||||||
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
|
const userTrust = cryptoEnabled && cli.checkUserTrust(member.userId);
|
||||||
const userVerified = cryptoEnabled && userTrust.isCrossSigningVerified();
|
const userVerified = cryptoEnabled && userTrust && userTrust.isCrossSigningVerified();
|
||||||
const isMe = member.userId === cli.getUserId();
|
const isMe = member.userId === cli.getUserId();
|
||||||
const canVerify =
|
const canVerify =
|
||||||
cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices && devices.length > 0;
|
cryptoEnabled && homeserverSupportsCrossSigning && !userVerified && !isMe && devices && devices.length > 0;
|
||||||
|
|
||||||
const setUpdating = (updating) => {
|
const setUpdating: SetUpdating = (updating) => {
|
||||||
setPendingUpdateCount((count) => count + (updating ? 1 : -1));
|
setPendingUpdateCount((count) => count + (updating ? 1 : -1));
|
||||||
};
|
};
|
||||||
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
|
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
|
||||||
|
@ -1408,9 +1427,9 @@ const BasicUserInfo: React.FC<{
|
||||||
|
|
||||||
export type Member = User | RoomMember;
|
export type Member = User | RoomMember;
|
||||||
|
|
||||||
const UserInfoHeader: React.FC<{
|
export const UserInfoHeader: React.FC<{
|
||||||
member: Member;
|
member: Member;
|
||||||
e2eStatus: E2EStatus;
|
e2eStatus?: E2EStatus;
|
||||||
roomId?: string;
|
roomId?: string;
|
||||||
}> = ({ member, e2eStatus, roomId }) => {
|
}> = ({ member, e2eStatus, roomId }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
@ -1427,9 +1446,11 @@ const UserInfoHeader: React.FC<{
|
||||||
name: (member as RoomMember).name || (member as User).displayName,
|
name: (member as RoomMember).name || (member as User).displayName,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", undefined, true);
|
||||||
}, [member]);
|
}, [member]);
|
||||||
|
|
||||||
|
const avatarUrl = (member as User).avatarUrl;
|
||||||
|
|
||||||
const avatarElement = (
|
const avatarElement = (
|
||||||
<div className="mx_UserInfo_avatar">
|
<div className="mx_UserInfo_avatar">
|
||||||
<div className="mx_UserInfo_avatar_transition">
|
<div className="mx_UserInfo_avatar_transition">
|
||||||
|
@ -1442,7 +1463,7 @@ const UserInfoHeader: React.FC<{
|
||||||
resizeMethod="scale"
|
resizeMethod="scale"
|
||||||
fallbackUserId={member.userId}
|
fallbackUserId={member.userId}
|
||||||
onClick={onMemberAvatarClick}
|
onClick={onMemberAvatarClick}
|
||||||
urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined}
|
urls={avatarUrl ? [avatarUrl] : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1475,10 +1496,7 @@ const UserInfoHeader: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let e2eIcon;
|
const e2eIcon = e2eStatus ? <E2EIcon size={18} status={e2eStatus} isUser={true} /> : null;
|
||||||
if (e2eStatus) {
|
|
||||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayName = (member as RoomMember).rawDisplayName;
|
const displayName = (member as RoomMember).rawDisplayName;
|
||||||
return (
|
return (
|
||||||
|
@ -1496,7 +1514,7 @@ const UserInfoHeader: React.FC<{
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_UserInfo_profile_mxid">
|
<div className="mx_UserInfo_profile_mxid">
|
||||||
{UserIdentifierCustomisations.getDisplayUserIdentifier(member.userId, {
|
{UserIdentifierCustomisations.getDisplayUserIdentifier?.(member.userId, {
|
||||||
roomId,
|
roomId,
|
||||||
withDisplayName: true,
|
withDisplayName: true,
|
||||||
})}
|
})}
|
||||||
|
@ -1533,7 +1551,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||||
|
|
||||||
const classes = ["mx_UserInfo"];
|
const classes = ["mx_UserInfo"];
|
||||||
|
|
||||||
let cardState: IRightPanelCardState;
|
let cardState: IRightPanelCardState = {};
|
||||||
// We have no previousPhase for when viewing a UserInfo without a Room at this time
|
// We have no previousPhase for when viewing a UserInfo without a Room at this time
|
||||||
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
if (room && phase === RightPanelPhases.EncryptionPanel) {
|
||||||
cardState = { member };
|
cardState = { member };
|
||||||
|
@ -1551,10 +1569,10 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||||
case RightPanelPhases.SpaceMemberInfo:
|
case RightPanelPhases.SpaceMemberInfo:
|
||||||
content = (
|
content = (
|
||||||
<BasicUserInfo
|
<BasicUserInfo
|
||||||
room={room}
|
room={room as Room}
|
||||||
member={member as User}
|
member={member as User}
|
||||||
devices={devices}
|
devices={devices as IDevice[]}
|
||||||
isRoomEncrypted={isRoomEncrypted}
|
isRoomEncrypted={Boolean(isRoomEncrypted)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -1565,7 +1583,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||||
{...(props as React.ComponentProps<typeof EncryptionPanel>)}
|
{...(props as React.ComponentProps<typeof EncryptionPanel>)}
|
||||||
member={member as User | RoomMember}
|
member={member as User | RoomMember}
|
||||||
onClose={onEncryptionPanelClose}
|
onClose={onEncryptionPanelClose}
|
||||||
isRoomEncrypted={isRoomEncrypted}
|
isRoomEncrypted={Boolean(isRoomEncrypted)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
@ -1582,7 +1600,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
|
||||||
let scopeHeader;
|
let scopeHeader;
|
||||||
if (room?.isSpaceRoom()) {
|
if (room?.isSpaceRoom()) {
|
||||||
scopeHeader = (
|
scopeHeader = (
|
||||||
<div data-test-id="space-header" className="mx_RightPanel_scopeHeader">
|
<div data-testid="space-header" className="mx_RightPanel_scopeHeader">
|
||||||
<RoomAvatar room={room} height={32} width={32} />
|
<RoomAvatar room={room} height={32} width={32} />
|
||||||
<RoomName room={room} />
|
<RoomName room={room} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,6 +58,8 @@ import { SendWysiwygComposer, sendMessage, getConversionFunctions } from "./wysi
|
||||||
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
import { MatrixClientProps, withMatrixClientHOC } from "../../../contexts/MatrixClientContext";
|
||||||
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
import { setUpVoiceBroadcastPreRecording } from "../../../voice-broadcast/utils/setUpVoiceBroadcastPreRecording";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
import { VoiceBroadcastInfoState } from "../../../voice-broadcast";
|
||||||
|
import { createCantStartVoiceMessageBroadcastDialog } from "../dialogs/CantStartVoiceMessageBroadcastDialog";
|
||||||
|
|
||||||
let instanceCount = 0;
|
let instanceCount = 0;
|
||||||
|
|
||||||
|
@ -445,6 +447,20 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onRecordStartEndClick = (): void => {
|
||||||
|
const currentBroadcastRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent();
|
||||||
|
|
||||||
|
if (currentBroadcastRecording && currentBroadcastRecording.getState() !== VoiceBroadcastInfoState.Stopped) {
|
||||||
|
createCantStartVoiceMessageBroadcastDialog();
|
||||||
|
} else {
|
||||||
|
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.context.narrow) {
|
||||||
|
this.toggleButtonMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
|
const hasE2EIcon = Boolean(!this.state.isWysiwygLabEnabled && this.props.e2eStatus);
|
||||||
const e2eIcon = hasE2EIcon && (
|
const e2eIcon = hasE2EIcon && (
|
||||||
|
@ -588,12 +604,7 @@ export class MessageComposer extends React.Component<IProps, IState> {
|
||||||
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
isStickerPickerOpen={this.state.isStickerPickerOpen}
|
||||||
menuPosition={menuPosition}
|
menuPosition={menuPosition}
|
||||||
relation={this.props.relation}
|
relation={this.props.relation}
|
||||||
onRecordStartEndClick={() => {
|
onRecordStartEndClick={this.onRecordStartEndClick}
|
||||||
this.voiceRecordingButton.current?.onRecordStartEndClick();
|
|
||||||
if (this.context.narrow) {
|
|
||||||
this.toggleButtonMenu();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
setStickerPickerOpen={this.setStickerPickerOpen}
|
setStickerPickerOpen={this.setStickerPickerOpen}
|
||||||
showLocationButton={!window.electron}
|
showLocationButton={!window.electron}
|
||||||
showPollsButton={this.state.showPollsButton}
|
showPollsButton={this.state.showPollsButton}
|
||||||
|
|
|
@ -376,8 +376,8 @@ function ComposerModeButton({ isRichTextEnabled, onClick }: WysiwygToggleButtonP
|
||||||
<CollapsibleButton
|
<CollapsibleButton
|
||||||
className="mx_MessageComposer_button"
|
className="mx_MessageComposer_button"
|
||||||
iconClassName={classNames({
|
iconClassName={classNames({
|
||||||
mx_MessageComposer_plain_text: isRichTextEnabled,
|
mx_MessageComposer_plain_text: !isRichTextEnabled,
|
||||||
mx_MessageComposer_rich_text: !isRichTextEnabled,
|
mx_MessageComposer_rich_text: isRichTextEnabled,
|
||||||
})}
|
})}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
title={title}
|
title={title}
|
||||||
|
|
|
@ -120,7 +120,7 @@ export function FormattingButtons({ composer, actionStates }: FormattingButtonsP
|
||||||
<Button
|
<Button
|
||||||
isActive={actionStates.link === "reversed"}
|
isActive={actionStates.link === "reversed"}
|
||||||
label={_td("Link")}
|
label={_td("Link")}
|
||||||
onClick={() => openLinkModal(composer, composerContext)}
|
onClick={() => openLinkModal(composer, composerContext, actionStates.link === "reversed")}
|
||||||
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
icon={<LinkIcon className="mx_FormattingButtons_Icon" />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,17 +17,28 @@ limitations under the License.
|
||||||
import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
import { FormattingFunctions } from "@matrix-org/matrix-wysiwyg";
|
||||||
import React, { ChangeEvent, useState } from "react";
|
import React, { ChangeEvent, useState } from "react";
|
||||||
|
|
||||||
import { _td } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
|
||||||
import Field from "../../../elements/Field";
|
import Field from "../../../elements/Field";
|
||||||
import { ComposerContextState } from "../ComposerContext";
|
import { ComposerContextState } from "../ComposerContext";
|
||||||
import { isSelectionEmpty, setSelection } from "../utils/selection";
|
import { isSelectionEmpty, setSelection } from "../utils/selection";
|
||||||
|
import BaseDialog from "../../../dialogs/BaseDialog";
|
||||||
|
import DialogButtons from "../../../elements/DialogButtons";
|
||||||
|
|
||||||
export function openLinkModal(composer: FormattingFunctions, composerContext: ComposerContextState) {
|
export function openLinkModal(
|
||||||
|
composer: FormattingFunctions,
|
||||||
|
composerContext: ComposerContextState,
|
||||||
|
isEditing: boolean,
|
||||||
|
) {
|
||||||
const modal = Modal.createDialog(
|
const modal = Modal.createDialog(
|
||||||
LinkModal,
|
LinkModal,
|
||||||
{ composerContext, composer, onClose: () => modal.close(), isTextEnabled: isSelectionEmpty() },
|
{
|
||||||
|
composerContext,
|
||||||
|
composer,
|
||||||
|
onClose: () => modal.close(),
|
||||||
|
isTextEnabled: isSelectionEmpty(),
|
||||||
|
isEditing,
|
||||||
|
},
|
||||||
"mx_CompoundDialog",
|
"mx_CompoundDialog",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
@ -43,48 +54,86 @@ interface LinkModalProps {
|
||||||
isTextEnabled: boolean;
|
isTextEnabled: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
composerContext: ComposerContextState;
|
composerContext: ComposerContextState;
|
||||||
|
isEditing: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LinkModal({ composer, isTextEnabled, onClose, composerContext }: LinkModalProps) {
|
export function LinkModal({ composer, isTextEnabled, onClose, composerContext, isEditing }: LinkModalProps) {
|
||||||
const [fields, setFields] = useState({ text: "", link: "" });
|
const [hasLinkChanged, setHasLinkChanged] = useState(false);
|
||||||
const isSaveDisabled = (isTextEnabled && isEmpty(fields.text)) || isEmpty(fields.link);
|
const [fields, setFields] = useState({ text: "", link: isEditing ? composer.getLink() : "" });
|
||||||
|
const hasText = !isEditing && isTextEnabled;
|
||||||
|
const isSaveDisabled = !hasLinkChanged || (hasText && isEmpty(fields.text)) || isEmpty(fields.link);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QuestionDialog
|
<BaseDialog
|
||||||
className="mx_LinkModal"
|
className="mx_LinkModal"
|
||||||
title={_td("Create a link")}
|
title={isEditing ? _t("Edit link") : _t("Create a link")}
|
||||||
button={_td("Save")}
|
hasCancel={true}
|
||||||
buttonDisabled={isSaveDisabled}
|
onFinished={onClose}
|
||||||
hasCancelButton={true}
|
>
|
||||||
onFinished={async (isClickOnSave: boolean) => {
|
<form
|
||||||
if (isClickOnSave) {
|
className="mx_LinkModal_content"
|
||||||
|
onSubmit={async (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
|
||||||
|
// When submitting is done when pressing enter when the link field has the focus,
|
||||||
|
// The link field is getting back the focus (due to react-focus-lock)
|
||||||
|
// So we are waiting that the focus stuff is done to play with the composer selection
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
|
|
||||||
await setSelection(composerContext.selection);
|
await setSelection(composerContext.selection);
|
||||||
composer.link(fields.link, isTextEnabled ? fields.text : undefined);
|
composer.link(fields.link, isTextEnabled ? fields.text : undefined);
|
||||||
}
|
|
||||||
onClose();
|
|
||||||
}}
|
}}
|
||||||
description={
|
>
|
||||||
<div className="mx_LinkModal_content">
|
{hasText && (
|
||||||
{isTextEnabled && (
|
|
||||||
<Field
|
<Field
|
||||||
|
required={true}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
label={_td("Text")}
|
label={_t("Text")}
|
||||||
value={fields.text}
|
value={fields.text}
|
||||||
|
className="mx_LinkModal_Field"
|
||||||
|
placeholder=""
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||||
setFields((fields) => ({ ...fields, text: e.target.value }))
|
setFields((fields) => ({ ...fields, text: e.target.value }))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Field
|
<Field
|
||||||
autoFocus={!isTextEnabled}
|
required={true}
|
||||||
label={_td("Link")}
|
autoFocus={!hasText}
|
||||||
|
label={_t("Link")}
|
||||||
value={fields.link}
|
value={fields.link}
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
className="mx_LinkModal_Field"
|
||||||
setFields((fields) => ({ ...fields, link: e.target.value }))
|
placeholder=""
|
||||||
}
|
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFields((fields) => ({ ...fields, link: e.target.value }));
|
||||||
|
setHasLinkChanged(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mx_LinkModal_buttons">
|
||||||
|
{isEditing && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="danger"
|
||||||
|
onClick={() => {
|
||||||
|
composer.removeLinks();
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{_t("Remove")}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Save")}
|
||||||
|
primaryDisabled={isSaveDisabled}
|
||||||
|
primaryIsSubmit={true}
|
||||||
|
onCancel={onClose}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
</form>
|
||||||
/>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { CryptoEvent } from "matrix-js-sdk/src/crypto";
|
||||||
|
|
||||||
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../contexts/MatrixClientContext";
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import { getDeviceClientInformation } from "../../../../utils/device/clientInformation";
|
import { getDeviceClientInformation, pruneClientInformation } from "../../../../utils/device/clientInformation";
|
||||||
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
import { DevicesDictionary, ExtendedDevice, ExtendedDeviceAppInfo } from "./types";
|
||||||
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../../hooks/useEventEmitter";
|
||||||
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
import { parseUserAgent } from "../../../../utils/device/parseUserAgent";
|
||||||
|
@ -116,8 +116,8 @@ export type DevicesState = {
|
||||||
export const useOwnDevices = (): DevicesState => {
|
export const useOwnDevices = (): DevicesState => {
|
||||||
const matrixClient = useContext(MatrixClientContext);
|
const matrixClient = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const currentDeviceId = matrixClient.getDeviceId();
|
const currentDeviceId = matrixClient.getDeviceId()!;
|
||||||
const userId = matrixClient.getUserId();
|
const userId = matrixClient.getSafeUserId();
|
||||||
|
|
||||||
const [devices, setDevices] = useState<DevicesState["devices"]>({});
|
const [devices, setDevices] = useState<DevicesState["devices"]>({});
|
||||||
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
|
const [pushers, setPushers] = useState<DevicesState["pushers"]>([]);
|
||||||
|
@ -138,11 +138,6 @@ export const useOwnDevices = (): DevicesState => {
|
||||||
const refreshDevices = useCallback(async () => {
|
const refreshDevices = useCallback(async () => {
|
||||||
setIsLoadingDeviceList(true);
|
setIsLoadingDeviceList(true);
|
||||||
try {
|
try {
|
||||||
// realistically we should never hit this
|
|
||||||
// but it satisfies types
|
|
||||||
if (!userId) {
|
|
||||||
throw new Error("Cannot fetch devices without user id");
|
|
||||||
}
|
|
||||||
const devices = await fetchDevicesWithVerification(matrixClient, userId);
|
const devices = await fetchDevicesWithVerification(matrixClient, userId);
|
||||||
setDevices(devices);
|
setDevices(devices);
|
||||||
|
|
||||||
|
@ -176,6 +171,15 @@ export const useOwnDevices = (): DevicesState => {
|
||||||
refreshDevices();
|
refreshDevices();
|
||||||
}, [refreshDevices]);
|
}, [refreshDevices]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const deviceIds = Object.keys(devices);
|
||||||
|
// empty devices means devices have not been fetched yet
|
||||||
|
// as there is always at least the current device
|
||||||
|
if (deviceIds.length) {
|
||||||
|
pruneClientInformation(deviceIds, matrixClient);
|
||||||
|
}
|
||||||
|
}, [devices, matrixClient]);
|
||||||
|
|
||||||
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
|
useEventEmitter(matrixClient, CryptoEvent.DevicesUpdated, (users: string[]): void => {
|
||||||
if (users.includes(userId)) {
|
if (users.includes(userId)) {
|
||||||
refreshDevices();
|
refreshDevices();
|
||||||
|
|
|
@ -15,12 +15,13 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
import { NotificationCount, NotificationCountType, Room, RoomEvent } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { Thread } from "matrix-js-sdk/src/models/thread";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { getUnsentMessages } from "../components/structures/RoomStatusBar";
|
import { getUnsentMessages } from "../components/structures/RoomStatusBar";
|
||||||
import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs";
|
import { getRoomNotifsState, getUnreadNotificationCount, RoomNotifState } from "../RoomNotifs";
|
||||||
import { NotificationColor } from "../stores/notifications/NotificationColor";
|
import { NotificationColor } from "../stores/notifications/NotificationColor";
|
||||||
import { doesRoomHaveUnreadMessages } from "../Unread";
|
import { doesRoomOrThreadHaveUnreadMessages } from "../Unread";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
import { EffectiveMembership, getEffectiveMembership } from "../utils/membership";
|
||||||
import { useEventEmitter } from "./useEventEmitter";
|
import { useEventEmitter } from "./useEventEmitter";
|
||||||
|
|
||||||
|
@ -75,12 +76,14 @@ export const useUnreadNotifications = (
|
||||||
setColor(NotificationColor.Red);
|
setColor(NotificationColor.Red);
|
||||||
} else if (greyNotifs > 0) {
|
} else if (greyNotifs > 0) {
|
||||||
setColor(NotificationColor.Grey);
|
setColor(NotificationColor.Grey);
|
||||||
} else if (!threadId) {
|
} else {
|
||||||
// TODO: No support for `Bold` on threads at the moment
|
|
||||||
|
|
||||||
// We don't have any notified messages, but we might have unread messages. Let's
|
// We don't have any notified messages, but we might have unread messages. Let's
|
||||||
// find out.
|
// find out.
|
||||||
const hasUnread = doesRoomHaveUnreadMessages(room);
|
let roomOrThread: Room | Thread = room;
|
||||||
|
if (threadId) {
|
||||||
|
roomOrThread = room.getThread(threadId)!;
|
||||||
|
}
|
||||||
|
const hasUnread = doesRoomOrThreadHaveUnreadMessages(roomOrThread);
|
||||||
setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None);
|
setColor(hasUnread ? NotificationColor.Bold : NotificationColor.None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -650,6 +650,8 @@
|
||||||
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.",
|
"You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.": "You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.",
|
||||||
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.",
|
"You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.": "You don't have the required permissions to start a voice broadcast in this room. Contact a room administrator to upgrade your permissions.",
|
||||||
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.",
|
"Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.": "Someone else is already recording a voice broadcast. Wait for their voice broadcast to end to start a new one.",
|
||||||
|
"Connection error": "Connection error",
|
||||||
|
"Unfortunately we're unable to start a recording right now. Please try again later.": "Unfortunately we're unable to start a recording right now. Please try again later.",
|
||||||
"Can’t start a call": "Can’t start a call",
|
"Can’t start a call": "Can’t start a call",
|
||||||
"You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.",
|
"You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.": "You can’t start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.",
|
||||||
"You ended a <a>voice broadcast</a>": "You ended a <a>voice broadcast</a>",
|
"You ended a <a>voice broadcast</a>": "You ended a <a>voice broadcast</a>",
|
||||||
|
@ -936,7 +938,6 @@
|
||||||
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
|
"Show message previews for reactions in DMs": "Show message previews for reactions in DMs",
|
||||||
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
"Show message previews for reactions in all rooms": "Show message previews for reactions in all rooms",
|
||||||
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
|
"Offline encrypted messaging using dehydrated devices": "Offline encrypted messaging using dehydrated devices",
|
||||||
"Show extensible event representation of events": "Show extensible event representation of events",
|
|
||||||
"Show current avatar and name for users in message history": "Show current avatar and name for users in message history",
|
"Show current avatar and name for users in message history": "Show current avatar and name for users in message history",
|
||||||
"Show HTML representation of room topics": "Show HTML representation of room topics",
|
"Show HTML representation of room topics": "Show HTML representation of room topics",
|
||||||
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
"Show info about bridges in room settings": "Show info about bridges in room settings",
|
||||||
|
@ -2135,6 +2136,7 @@
|
||||||
"Underline": "Underline",
|
"Underline": "Underline",
|
||||||
"Code": "Code",
|
"Code": "Code",
|
||||||
"Link": "Link",
|
"Link": "Link",
|
||||||
|
"Edit link": "Edit link",
|
||||||
"Create a link": "Create a link",
|
"Create a link": "Create a link",
|
||||||
"Text": "Text",
|
"Text": "Text",
|
||||||
"Message Actions": "Message Actions",
|
"Message Actions": "Message Actions",
|
||||||
|
@ -2686,6 +2688,8 @@
|
||||||
"Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)",
|
"Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)": "Uncheck if you also want to remove system messages on this user (e.g. membership change, profile change…)",
|
||||||
"Remove %(count)s messages|other": "Remove %(count)s messages",
|
"Remove %(count)s messages|other": "Remove %(count)s messages",
|
||||||
"Remove %(count)s messages|one": "Remove 1 message",
|
"Remove %(count)s messages|one": "Remove 1 message",
|
||||||
|
"Can't start voice message": "Can't start voice message",
|
||||||
|
"You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.": "You can't start a voice message as you are currently recording a live broadcast. Please end your live broadcast in order to start recording a voice message.",
|
||||||
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
|
"Unable to load commit detail: %(msg)s": "Unable to load commit detail: %(msg)s",
|
||||||
"Unavailable": "Unavailable",
|
"Unavailable": "Unavailable",
|
||||||
"Changelog": "Changelog",
|
"Changelog": "Changelog",
|
||||||
|
|
|
@ -340,13 +340,6 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
"feature_extensible_events": {
|
|
||||||
isFeature: true,
|
|
||||||
labsGroup: LabGroup.Developer, // developer for now, eventually Messaging and default on
|
|
||||||
supportedLevels: LEVELS_FEATURE,
|
|
||||||
displayName: _td("Show extensible event representation of events"),
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
"useOnlyCurrentProfiles": {
|
"useOnlyCurrentProfiles": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("Show current avatar and name for users in message history"),
|
displayName: _td("Show current avatar and name for users in message history"),
|
||||||
|
|
|
@ -40,8 +40,8 @@ const formatUrl = (): string | undefined => {
|
||||||
].join("");
|
].join("");
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getClientInformationEventType = (deviceId: string): string =>
|
const clientInformationEventPrefix = "io.element.matrix_client_information.";
|
||||||
`io.element.matrix_client_information.${deviceId}`;
|
export const getClientInformationEventType = (deviceId: string): string => `${clientInformationEventPrefix}${deviceId}`;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record extra client information for the current device
|
* Record extra client information for the current device
|
||||||
|
@ -52,7 +52,7 @@ export const recordClientInformation = async (
|
||||||
sdkConfig: IConfigOptions,
|
sdkConfig: IConfigOptions,
|
||||||
platform: BasePlatform,
|
platform: BasePlatform,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const deviceId = matrixClient.getDeviceId();
|
const deviceId = matrixClient.getDeviceId()!;
|
||||||
const { brand } = sdkConfig;
|
const { brand } = sdkConfig;
|
||||||
const version = await platform.getAppVersion();
|
const version = await platform.getAppVersion();
|
||||||
const type = getClientInformationEventType(deviceId);
|
const type = getClientInformationEventType(deviceId);
|
||||||
|
@ -66,12 +66,27 @@ export const recordClientInformation = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove extra client information
|
* Remove client information events for devices that no longer exist
|
||||||
* @todo(kerrya) revisit after MSC3391: account data deletion is done
|
* @param validDeviceIds - ids of current devices,
|
||||||
* (PSBE-12)
|
* client information for devices NOT in this list will be removed
|
||||||
|
*/
|
||||||
|
export const pruneClientInformation = (validDeviceIds: string[], matrixClient: MatrixClient): void => {
|
||||||
|
Object.values(matrixClient.store.accountData).forEach((event) => {
|
||||||
|
if (!event.getType().startsWith(clientInformationEventPrefix)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [, deviceId] = event.getType().split(clientInformationEventPrefix);
|
||||||
|
if (deviceId && !validDeviceIds.includes(deviceId)) {
|
||||||
|
matrixClient.deleteAccountData(event.getType());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove extra client information for current device
|
||||||
*/
|
*/
|
||||||
export const removeClientInformation = async (matrixClient: MatrixClient): Promise<void> => {
|
export const removeClientInformation = async (matrixClient: MatrixClient): Promise<void> => {
|
||||||
const deviceId = matrixClient.getDeviceId();
|
const deviceId = matrixClient.getDeviceId()!;
|
||||||
const type = getClientInformationEventType(deviceId);
|
const type = getClientInformationEventType(deviceId);
|
||||||
const clientInformation = getDeviceClientInformation(matrixClient, deviceId);
|
const clientInformation = getDeviceClientInformation(matrixClient, deviceId);
|
||||||
|
|
||||||
|
|
|
@ -60,13 +60,20 @@ export class VoiceBroadcastRecording
|
||||||
{
|
{
|
||||||
private state: VoiceBroadcastInfoState;
|
private state: VoiceBroadcastInfoState;
|
||||||
private recorder: VoiceBroadcastRecorder;
|
private recorder: VoiceBroadcastRecorder;
|
||||||
private sequence = 1;
|
|
||||||
private dispatcherRef: string;
|
private dispatcherRef: string;
|
||||||
private chunkEvents = new VoiceBroadcastChunkEvents();
|
private chunkEvents = new VoiceBroadcastChunkEvents();
|
||||||
private chunkRelationHelper: RelationsHelper;
|
private chunkRelationHelper: RelationsHelper;
|
||||||
private maxLength: number;
|
private maxLength: number;
|
||||||
private timeLeft: number;
|
private timeLeft: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Broadcast chunks have a sequence number to bring them in the correct order and to know if a message is missing.
|
||||||
|
* This variable holds the last sequence number.
|
||||||
|
* Starts with 0 because there is no chunk at the beginning of a broadcast.
|
||||||
|
* Will be incremented when a chunk message is created.
|
||||||
|
*/
|
||||||
|
private sequence = 0;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly infoEvent: MatrixEvent,
|
public readonly infoEvent: MatrixEvent,
|
||||||
private client: MatrixClient,
|
private client: MatrixClient,
|
||||||
|
@ -268,7 +275,8 @@ export class VoiceBroadcastRecording
|
||||||
event_id: this.infoEvent.getId(),
|
event_id: this.infoEvent.getId(),
|
||||||
};
|
};
|
||||||
content["io.element.voice_broadcast_chunk"] = {
|
content["io.element.voice_broadcast_chunk"] = {
|
||||||
sequence: this.sequence++,
|
/** Increment the last sequence number and use it for this message. Also see {@link sequence}. */
|
||||||
|
sequence: ++this.sequence,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.client.sendMessage(this.infoEvent.getRoomId(), content);
|
await this.client.sendMessage(this.infoEvent.getRoomId(), content);
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from "..";
|
import { hasRoomLiveVoiceBroadcast, VoiceBroadcastInfoEventType, VoiceBroadcastRecordingsStore } from "..";
|
||||||
import InfoDialog from "../../components/views/dialogs/InfoDialog";
|
import InfoDialog from "../../components/views/dialogs/InfoDialog";
|
||||||
|
@ -67,6 +68,14 @@ const showOthersAlreadyRecordingDialog = () => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showNoConnectionDialog = (): void => {
|
||||||
|
Modal.createDialog(InfoDialog, {
|
||||||
|
title: _t("Connection error"),
|
||||||
|
description: <p>{_t("Unfortunately we're unable to start a recording right now. Please try again later.")}</p>,
|
||||||
|
hasCloseButton: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const checkVoiceBroadcastPreConditions = async (
|
export const checkVoiceBroadcastPreConditions = async (
|
||||||
room: Room,
|
room: Room,
|
||||||
client: MatrixClient,
|
client: MatrixClient,
|
||||||
|
@ -86,6 +95,11 @@ export const checkVoiceBroadcastPreConditions = async (
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (client.getSyncState() === SyncState.Error) {
|
||||||
|
showNoConnectionDialog();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId);
|
const { hasBroadcast, startedByUser } = await hasRoomLiveVoiceBroadcast(client, room, currentUserId);
|
||||||
|
|
||||||
if (hasBroadcast && startedByUser) {
|
if (hasBroadcast && startedByUser) {
|
||||||
|
|
|
@ -15,25 +15,26 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { MatrixEvent, EventType, MsgType } from "matrix-js-sdk/src/matrix";
|
import { MatrixEvent, EventType, MsgType, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||||
|
|
||||||
import { haveRendererForEvent } from "../src/events/EventTileFactory";
|
import { haveRendererForEvent } from "../src/events/EventTileFactory";
|
||||||
import { getMockClientWithEventEmitter, makeBeaconEvent, mockClientMethodsUser } from "./test-utils";
|
import { makeBeaconEvent, mkEvent, stubClient } from "./test-utils";
|
||||||
import { eventTriggersUnreadCount } from "../src/Unread";
|
import { mkThread } from "./test-utils/threads";
|
||||||
|
import { doesRoomHaveUnreadMessages, eventTriggersUnreadCount } from "../src/Unread";
|
||||||
|
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||||
|
|
||||||
jest.mock("../src/events/EventTileFactory", () => ({
|
jest.mock("../src/events/EventTileFactory", () => ({
|
||||||
haveRendererForEvent: jest.fn(),
|
haveRendererForEvent: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("eventTriggersUnreadCount()", () => {
|
describe("Unread", () => {
|
||||||
|
// A different user.
|
||||||
const aliceId = "@alice:server.org";
|
const aliceId = "@alice:server.org";
|
||||||
const bobId = "@bob:server.org";
|
stubClient();
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
// mock user credentials
|
|
||||||
getMockClientWithEventEmitter({
|
|
||||||
...mockClientMethodsUser(bobId),
|
|
||||||
});
|
|
||||||
|
|
||||||
|
describe("eventTriggersUnreadCount()", () => {
|
||||||
// setup events
|
// setup events
|
||||||
const alicesMessage = new MatrixEvent({
|
const alicesMessage = new MatrixEvent({
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
|
@ -44,9 +45,9 @@ describe("eventTriggersUnreadCount()", () => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const bobsMessage = new MatrixEvent({
|
const ourMessage = new MatrixEvent({
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
sender: bobId,
|
sender: client.getUserId()!,
|
||||||
content: {
|
content: {
|
||||||
msgtype: MsgType.Text,
|
msgtype: MsgType.Text,
|
||||||
body: "Hello from Bob",
|
body: "Hello from Bob",
|
||||||
|
@ -65,7 +66,7 @@ describe("eventTriggersUnreadCount()", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns false when the event was sent by the current user", () => {
|
it("returns false when the event was sent by the current user", () => {
|
||||||
expect(eventTriggersUnreadCount(bobsMessage)).toBe(false);
|
expect(eventTriggersUnreadCount(ourMessage)).toBe(false);
|
||||||
// returned early before checking renderer
|
// returned early before checking renderer
|
||||||
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -103,12 +104,217 @@ describe("eventTriggersUnreadCount()", () => {
|
||||||
EventType.RoomServerAcl,
|
EventType.RoomServerAcl,
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(noUnreadEventTypes)("returns false without checking for renderer for events with type %s", (eventType) => {
|
it.each(noUnreadEventTypes)(
|
||||||
|
"returns false without checking for renderer for events with type %s",
|
||||||
|
(eventType) => {
|
||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
type: eventType,
|
type: eventType,
|
||||||
sender: aliceId,
|
sender: aliceId,
|
||||||
});
|
});
|
||||||
expect(eventTriggersUnreadCount(event)).toBe(false);
|
expect(eventTriggersUnreadCount(event)).toBe(false);
|
||||||
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("doesRoomHaveUnreadMessages()", () => {
|
||||||
|
let room: Room;
|
||||||
|
let event: MatrixEvent;
|
||||||
|
const roomId = "!abc:server.org";
|
||||||
|
const myId = client.getUserId()!;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
client.supportsExperimentalThreads = () => true;
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Create a room and initial event in it.
|
||||||
|
room = new Room(roomId, client, myId);
|
||||||
|
event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.message",
|
||||||
|
user: aliceId,
|
||||||
|
room: roomId,
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
|
||||||
|
// Don't care about the code path of hidden events.
|
||||||
|
mocked(haveRendererForEvent).mockClear().mockReturnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for a room with no receipts", () => {
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a room when the latest event was sent by the current user", () => {
|
||||||
|
event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.message",
|
||||||
|
user: myId,
|
||||||
|
room: roomId,
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
// Only for timeline events.
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a room when the read receipt is at the latest event", () => {
|
||||||
|
const receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for a room when the read receipt is earlier than the latest event", () => {
|
||||||
|
const receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
const event2 = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.message",
|
||||||
|
user: aliceId,
|
||||||
|
room: roomId,
|
||||||
|
content: {},
|
||||||
|
});
|
||||||
|
// Only for timeline events.
|
||||||
|
room.addLiveEvents([event2]);
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for a room with an unread message in a thread", () => {
|
||||||
|
// Mark the main timeline as read.
|
||||||
|
const receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
// Create a thread as a different user.
|
||||||
|
mkThread({ room, client, authorId: myId, participantUserIds: [aliceId] });
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a room when the latest thread event was sent by the current user", () => {
|
||||||
|
// Mark the main timeline as read.
|
||||||
|
const receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
// Create a thread as the current user.
|
||||||
|
mkThread({ room, client, authorId: myId, participantUserIds: [myId] });
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for a room with read thread messages", () => {
|
||||||
|
// Mark the main timeline as read.
|
||||||
|
let receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
// Create threads.
|
||||||
|
const { rootEvent, events } = mkThread({ room, client, authorId: myId, participantUserIds: [aliceId] });
|
||||||
|
|
||||||
|
// Mark the thread as read.
|
||||||
|
receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[events[events.length - 1].getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1, thread_id: rootEvent.getId()! },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for a room when read receipt is not on the latest thread messages", () => {
|
||||||
|
// Mark the main timeline as read.
|
||||||
|
let receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
// Create threads.
|
||||||
|
const { rootEvent, events } = mkThread({ room, client, authorId: myId, participantUserIds: [aliceId] });
|
||||||
|
|
||||||
|
// Mark the thread as read.
|
||||||
|
receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: "!foo:bar",
|
||||||
|
content: {
|
||||||
|
[events[0].getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[myId]: { ts: 1, threadId: rootEvent.getId()! },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
|
expect(doesRoomHaveUnreadMessages(room)).toBe(true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
import { render, RenderResult, waitFor, screen } from "@testing-library/react";
|
import { render, RenderResult, waitFor, screen } from "@testing-library/react";
|
||||||
// eslint-disable-next-line deprecate/import
|
// eslint-disable-next-line deprecate/import
|
||||||
import { mount, ReactWrapper } from "enzyme";
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
import { MessageEvent } from "matrix-events-sdk";
|
|
||||||
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||||
import {
|
import {
|
||||||
EventTimelineSet,
|
EventTimelineSet,
|
||||||
|
@ -48,6 +47,7 @@ import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
import { isCallEvent } from "../../../src/components/structures/LegacyCallEventGrouper";
|
import { isCallEvent } from "../../../src/components/structures/LegacyCallEventGrouper";
|
||||||
import { flushPromises, mkMembership, mkRoom, stubClient } from "../../test-utils";
|
import { flushPromises, mkMembership, mkRoom, stubClient } from "../../test-utils";
|
||||||
import { mkThread } from "../../test-utils/threads";
|
import { mkThread } from "../../test-utils/threads";
|
||||||
|
import { createMessageEventContent } from "../../test-utils/events";
|
||||||
|
|
||||||
const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => {
|
const newReceipt = (eventId: string, userId: string, readTs: number, fullyReadTs: number): MatrixEvent => {
|
||||||
const receiptContent = {
|
const receiptContent = {
|
||||||
|
@ -89,8 +89,8 @@ const mockEvents = (room: Room, count = 2): MatrixEvent[] => {
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
event_id: `${room.roomId}_event_${index}`,
|
event_id: `${room.roomId}_event_${index}`,
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
user_id: "userId",
|
sender: "userId",
|
||||||
content: MessageEvent.from(`Event${index}`).serialize().content,
|
content: createMessageEventContent("`Event${index}`"),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -125,13 +125,15 @@ describe("TimelinePanel", () => {
|
||||||
event_id: "ev0",
|
event_id: "ev0",
|
||||||
sender: "@u2:m.org",
|
sender: "@u2:m.org",
|
||||||
origin_server_ts: 111,
|
origin_server_ts: 111,
|
||||||
...MessageEvent.from("hello 1").serialize(),
|
type: EventType.RoomMessage,
|
||||||
|
content: createMessageEventContent("hello 1"),
|
||||||
});
|
});
|
||||||
const ev1 = new MatrixEvent({
|
const ev1 = new MatrixEvent({
|
||||||
event_id: "ev1",
|
event_id: "ev1",
|
||||||
sender: "@u2:m.org",
|
sender: "@u2:m.org",
|
||||||
origin_server_ts: 222,
|
origin_server_ts: 222,
|
||||||
...MessageEvent.from("hello 2").serialize(),
|
type: EventType.RoomMessage,
|
||||||
|
content: createMessageEventContent("hello 2"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const roomId = "#room:example.com";
|
const roomId = "#room:example.com";
|
||||||
|
@ -385,24 +387,24 @@ describe("TimelinePanel", () => {
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
event_id: "event_reply_1",
|
event_id: "event_reply_1",
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
user_id: "userId",
|
sender: "userId",
|
||||||
content: MessageEvent.from(`ReplyEvent1`).serialize().content,
|
content: createMessageEventContent("ReplyEvent1"),
|
||||||
});
|
});
|
||||||
|
|
||||||
reply2 = new MatrixEvent({
|
reply2 = new MatrixEvent({
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
event_id: "event_reply_2",
|
event_id: "event_reply_2",
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
user_id: "userId",
|
sender: "userId",
|
||||||
content: MessageEvent.from(`ReplyEvent2`).serialize().content,
|
content: createMessageEventContent("ReplyEvent2"),
|
||||||
});
|
});
|
||||||
|
|
||||||
root = new MatrixEvent({
|
root = new MatrixEvent({
|
||||||
room_id: room.roomId,
|
room_id: room.roomId,
|
||||||
event_id: "event_root_1",
|
event_id: "event_root_1",
|
||||||
type: EventType.RoomMessage,
|
type: EventType.RoomMessage,
|
||||||
user_id: "userId",
|
sender: "userId",
|
||||||
content: MessageEvent.from(`RootEvent`).serialize().content,
|
content: createMessageEventContent("RootEvent"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventMap: { [key: string]: MatrixEvent } = {
|
const eventMap: { [key: string]: MatrixEvent } = {
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {
|
||||||
getBeaconInfoIdentifier,
|
getBeaconInfoIdentifier,
|
||||||
EventType,
|
EventType,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { ExtensibleEvent, MessageEvent, M_POLL_KIND_DISCLOSED, PollStartEvent } from "matrix-events-sdk";
|
import { M_POLL_KIND_DISCLOSED, PollStartEvent } from "matrix-events-sdk";
|
||||||
import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
|
import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { act } from "@testing-library/react";
|
import { act } from "@testing-library/react";
|
||||||
|
@ -44,6 +44,7 @@ import { ReadPinsEventId } from "../../../../src/components/views/right_panel/ty
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||||
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
import { VoiceBroadcastInfoState } from "../../../../src/voice-broadcast";
|
||||||
|
import { createMessageEventContent } from "../../../test-utils/events";
|
||||||
|
|
||||||
jest.mock("../../../../src/utils/strings", () => ({
|
jest.mock("../../../../src/utils/strings", () => ({
|
||||||
copyPlaintext: jest.fn(),
|
copyPlaintext: jest.fn(),
|
||||||
|
@ -64,7 +65,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does show copy link button when supplied a link", () => {
|
it("does show copy link button when supplied a link", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const props = {
|
const props = {
|
||||||
link: "https://google.com/",
|
link: "https://google.com/",
|
||||||
};
|
};
|
||||||
|
@ -75,7 +76,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show copy link button when not supplied a link", () => {
|
it("does not show copy link button when not supplied a link", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const menu = createMenuWithContent(eventContent);
|
const menu = createMenuWithContent(eventContent);
|
||||||
const copyLinkButton = menu.find('a[aria-label="Copy link"]');
|
const copyLinkButton = menu.find('a[aria-label="Copy link"]');
|
||||||
expect(copyLinkButton).toHaveLength(0);
|
expect(copyLinkButton).toHaveLength(0);
|
||||||
|
@ -91,8 +92,8 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show pin option when user does not have rights to pin", () => {
|
it("does not show pin option when user does not have rights to pin", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const event = new MatrixEvent(eventContent.serialize());
|
const event = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
const room = makeDefaultRoom();
|
||||||
// mock permission to disallow adding pinned messages to room
|
// mock permission to disallow adding pinned messages to room
|
||||||
|
@ -116,8 +117,12 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show pin option when pinning feature is disabled", () => {
|
it("does not show pin option when pinning feature is disabled", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
const pinnableEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: eventContent,
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
const room = makeDefaultRoom();
|
||||||
// mock permission to allow adding pinned messages to room
|
// mock permission to allow adding pinned messages to room
|
||||||
|
@ -131,8 +136,12 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows pin option when pinning feature is enabled", () => {
|
it("shows pin option when pinning feature is enabled", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
const pinnableEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: eventContent,
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
|
|
||||||
const room = makeDefaultRoom();
|
const room = makeDefaultRoom();
|
||||||
// mock permission to allow adding pinned messages to room
|
// mock permission to allow adding pinned messages to room
|
||||||
|
@ -145,8 +154,12 @@ describe("MessageContextMenu", () => {
|
||||||
|
|
||||||
it("pins event on pin option click", () => {
|
it("pins event on pin option click", () => {
|
||||||
const onFinished = jest.fn();
|
const onFinished = jest.fn();
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
const pinnableEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: eventContent,
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
pinnableEvent.event.event_id = "!3";
|
pinnableEvent.event.event_id = "!3";
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = makeDefaultRoom();
|
const room = makeDefaultRoom();
|
||||||
|
@ -188,8 +201,12 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unpins event on pin option click when event is pinned", () => {
|
it("unpins event on pin option click when event is pinned", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const pinnableEvent = new MatrixEvent({ ...eventContent.serialize(), room_id: roomId });
|
const pinnableEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
content: eventContent,
|
||||||
|
room_id: roomId,
|
||||||
|
});
|
||||||
pinnableEvent.event.event_id = "!3";
|
pinnableEvent.event.event_id = "!3";
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = makeDefaultRoom();
|
const room = makeDefaultRoom();
|
||||||
|
@ -231,7 +248,7 @@ describe("MessageContextMenu", () => {
|
||||||
|
|
||||||
describe("message forwarding", () => {
|
describe("message forwarding", () => {
|
||||||
it("allows forwarding a room message", () => {
|
it("allows forwarding a room message", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const menu = createMenuWithContent(eventContent);
|
const menu = createMenuWithContent(eventContent);
|
||||||
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(1);
|
expect(menu.find('div[aria-label="Forward"]')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
@ -335,7 +352,7 @@ describe("MessageContextMenu", () => {
|
||||||
|
|
||||||
describe("open as map link", () => {
|
describe("open as map link", () => {
|
||||||
it("does not allow opening a plain message in open street maps", () => {
|
it("does not allow opening a plain message in open street maps", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const menu = createMenuWithContent(eventContent);
|
const menu = createMenuWithContent(eventContent);
|
||||||
expect(menu.find('a[aria-label="Open in OpenStreetMap"]')).toHaveLength(0);
|
expect(menu.find('a[aria-label="Open in OpenStreetMap"]')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
@ -380,7 +397,7 @@ describe("MessageContextMenu", () => {
|
||||||
describe("right click", () => {
|
describe("right click", () => {
|
||||||
it("copy button does work as expected", () => {
|
it("copy button does work as expected", () => {
|
||||||
const text = "hello";
|
const text = "hello";
|
||||||
const eventContent = MessageEvent.from(text);
|
const eventContent = createMessageEventContent(text);
|
||||||
mocked(getSelectedText).mockReturnValue(text);
|
mocked(getSelectedText).mockReturnValue(text);
|
||||||
|
|
||||||
const menu = createRightClickMenuWithContent(eventContent);
|
const menu = createRightClickMenuWithContent(eventContent);
|
||||||
|
@ -391,7 +408,7 @@ describe("MessageContextMenu", () => {
|
||||||
|
|
||||||
it("copy button is not shown when there is nothing to copy", () => {
|
it("copy button is not shown when there is nothing to copy", () => {
|
||||||
const text = "hello";
|
const text = "hello";
|
||||||
const eventContent = MessageEvent.from(text);
|
const eventContent = createMessageEventContent(text);
|
||||||
mocked(getSelectedText).mockReturnValue("");
|
mocked(getSelectedText).mockReturnValue("");
|
||||||
|
|
||||||
const menu = createRightClickMenuWithContent(eventContent);
|
const menu = createRightClickMenuWithContent(eventContent);
|
||||||
|
@ -400,7 +417,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows edit button when we can edit", () => {
|
it("shows edit button when we can edit", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
mocked(canEditContent).mockReturnValue(true);
|
mocked(canEditContent).mockReturnValue(true);
|
||||||
|
|
||||||
const menu = createRightClickMenuWithContent(eventContent);
|
const menu = createRightClickMenuWithContent(eventContent);
|
||||||
|
@ -409,7 +426,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show edit button when we cannot edit", () => {
|
it("does not show edit button when we cannot edit", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
mocked(canEditContent).mockReturnValue(false);
|
mocked(canEditContent).mockReturnValue(false);
|
||||||
|
|
||||||
const menu = createRightClickMenuWithContent(eventContent);
|
const menu = createRightClickMenuWithContent(eventContent);
|
||||||
|
@ -418,7 +435,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows reply button when we can reply", () => {
|
it("shows reply button when we can reply", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const context = {
|
const context = {
|
||||||
canSendMessages: true,
|
canSendMessages: true,
|
||||||
};
|
};
|
||||||
|
@ -429,11 +446,11 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show reply button when we cannot reply", () => {
|
it("does not show reply button when we cannot reply", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const context = {
|
const context = {
|
||||||
canSendMessages: true,
|
canSendMessages: true,
|
||||||
};
|
};
|
||||||
const unsentMessage = new MatrixEvent(eventContent.serialize());
|
const unsentMessage = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||||
// queued messages are not actionable
|
// queued messages are not actionable
|
||||||
unsentMessage.setStatus(EventStatus.QUEUED);
|
unsentMessage.setStatus(EventStatus.QUEUED);
|
||||||
|
|
||||||
|
@ -443,7 +460,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows react button when we can react", () => {
|
it("shows react button when we can react", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const context = {
|
const context = {
|
||||||
canReact: true,
|
canReact: true,
|
||||||
};
|
};
|
||||||
|
@ -454,7 +471,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show react button when we cannot react", () => {
|
it("does not show react button when we cannot react", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const context = {
|
const context = {
|
||||||
canReact: false,
|
canReact: false,
|
||||||
};
|
};
|
||||||
|
@ -465,8 +482,8 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows view in room button when the event is a thread root", () => {
|
it("shows view in room button when the event is a thread root", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const mxEvent = new MatrixEvent(eventContent.serialize());
|
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||||
mxEvent.getThread = () => ({ rootEvent: mxEvent } as Thread);
|
mxEvent.getThread = () => ({ rootEvent: mxEvent } as Thread);
|
||||||
const props = {
|
const props = {
|
||||||
rightClick: true,
|
rightClick: true,
|
||||||
|
@ -481,7 +498,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not show view in room button when the event is not a thread root", () => {
|
it("does not show view in room button when the event is not a thread root", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
|
|
||||||
const menu = createRightClickMenuWithContent(eventContent);
|
const menu = createRightClickMenuWithContent(eventContent);
|
||||||
const reactButton = menu.find('div[aria-label="View in room"]');
|
const reactButton = menu.find('div[aria-label="View in room"]');
|
||||||
|
@ -489,8 +506,8 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates a new thread on reply in thread click", () => {
|
it("creates a new thread on reply in thread click", () => {
|
||||||
const eventContent = MessageEvent.from("hello");
|
const eventContent = createMessageEventContent("hello");
|
||||||
const mxEvent = new MatrixEvent(eventContent.serialize());
|
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||||
|
|
||||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||||
const context = {
|
const context = {
|
||||||
|
@ -513,7 +530,7 @@ describe("MessageContextMenu", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function createRightClickMenuWithContent(eventContent: ExtensibleEvent, context?: Partial<IRoomState>): ReactWrapper {
|
function createRightClickMenuWithContent(eventContent: object, context?: Partial<IRoomState>): ReactWrapper {
|
||||||
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
return createMenuWithContent(eventContent, { rightClick: true }, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -522,11 +539,13 @@ function createRightClickMenu(mxEvent: MatrixEvent, context?: Partial<IRoomState
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMenuWithContent(
|
function createMenuWithContent(
|
||||||
eventContent: ExtensibleEvent,
|
eventContent: object,
|
||||||
props?: Partial<React.ComponentProps<typeof MessageContextMenu>>,
|
props?: Partial<React.ComponentProps<typeof MessageContextMenu>>,
|
||||||
context?: Partial<IRoomState>,
|
context?: Partial<IRoomState>,
|
||||||
): ReactWrapper {
|
): ReactWrapper {
|
||||||
const mxEvent = new MatrixEvent(eventContent.serialize());
|
// XXX: We probably shouldn't be assuming all events are going to be message events, but considering this
|
||||||
|
// test is for the Message context menu, it's a fairly safe assumption.
|
||||||
|
const mxEvent = new MatrixEvent({ type: EventType.RoomMessage, content: eventContent });
|
||||||
return createMenu(mxEvent, props, context);
|
return createMenu(mxEvent, props, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,27 +21,7 @@ exports[`RoomGeneralContextMenu renders an empty context menu for archived rooms
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-checked="false"
|
|
||||||
aria-label="Mark as read"
|
|
||||||
class="mx_AccessibleButton mx_IconizedContextMenu_item"
|
|
||||||
role="menuitemcheckbox"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconMarkAsRead"
|
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
class="mx_IconizedContextMenu_label"
|
|
||||||
>
|
|
||||||
Mark as read
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="mx_IconizedContextMenu_icon mx_IconizedContextMenu_unchecked"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||||
>
|
>
|
||||||
|
@ -88,27 +68,7 @@ exports[`RoomGeneralContextMenu renders the default context menu 1`] = `
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst"
|
||||||
>
|
|
||||||
<div
|
|
||||||
aria-checked="false"
|
|
||||||
aria-label="Mark as read"
|
|
||||||
class="mx_AccessibleButton mx_IconizedContextMenu_item"
|
|
||||||
role="menuitemcheckbox"
|
|
||||||
tabindex="0"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_IconizedContextMenu_icon mx_RoomGeneralContextMenu_iconMarkAsRead"
|
|
||||||
/>
|
/>
|
||||||
<span
|
|
||||||
class="mx_IconizedContextMenu_label"
|
|
||||||
>
|
|
||||||
Mark as read
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="mx_IconizedContextMenu_icon mx_IconizedContextMenu_unchecked"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
class="mx_IconizedContextMenu_optionList mx_IconizedContextMenu_optionList_notFirst mx_IconizedContextMenu_optionList_red"
|
||||||
>
|
>
|
||||||
|
|
54
test/components/views/elements/Field-test.tsx
Normal file
54
test/components/views/elements/Field-test.tsx
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
|
import Field from "../../../../src/components/views/elements/Field";
|
||||||
|
|
||||||
|
describe("Field", () => {
|
||||||
|
describe("Placeholder", () => {
|
||||||
|
it("Should display a placeholder", async () => {
|
||||||
|
// When
|
||||||
|
const { rerender } = render(<Field value="" placeholder="my placeholder" />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my placeholder");
|
||||||
|
|
||||||
|
// When
|
||||||
|
rerender(<Field value="" placeholder="" />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should display label as placeholder", async () => {
|
||||||
|
// When
|
||||||
|
render(<Field value="" label="my label" />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole("textbox")).toHaveAttribute("placeholder", "my label");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should not display a placeholder", async () => {
|
||||||
|
// When
|
||||||
|
render(<Field value="" />);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByRole("textbox")).not.toHaveAttribute("placeholder", "my placeholder");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`PollCreateDialog renders a blank poll 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_1"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_1"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value=""><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" aria-disabled="true" disabled="" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
exports[`PollCreateDialog renders a blank poll 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_1"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_1"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value=""><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value=""><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" aria-disabled="true" disabled="" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary mx_AccessibleButton_disabled">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||||
|
|
||||||
exports[`PollCreateDialog renders a question and some options 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_4"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_4"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="How many turnips is the optimal number?"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="As many as my neighbour"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="The question is meaningless"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_2" maxlength="340" label="Option 3" placeholder="Write an option" type="text" value="Mu"><label for="pollcreate_option_2">Option 3</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
exports[`PollCreateDialog renders a question and some options 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Create poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_4"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_4"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="How many turnips is the optimal number?"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="As many as my neighbour"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="The question is meaningless"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_2" maxlength="340" label="Option 3" placeholder="Write an option" type="text" value="Mu"><label for="pollcreate_option_2">Option 3</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Create Poll</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||||
|
|
||||||
exports[`PollCreateDialog renders info from a previous event 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Edit poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_5"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_5"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="Poll Q"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="Answer 1"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="Answer 2"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Done</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
exports[`PollCreateDialog renders info from a previous event 1`] = `"<div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div><div data-focus-lock-disabled="false" role="dialog" aria-labelledby="mx_CompoundDialog_title" aria-describedby="mx_CompoundDialog_content" class="mx_CompoundDialog mx_ScrollableBaseDialog"><div class="mx_CompoundDialog_header"><h1>Edit poll</h1><div aria-label="Close dialog" role="button" tabindex="0" class="mx_AccessibleButton mx_CompoundDialog_cancelButton"></div></div><form class="mx_CompoundDialog_form"><div class="mx_CompoundDialog_content"><div class="mx_PollCreateDialog"><h2>Poll type</h2><div class="mx_Field mx_Field_select"><select type="text" id="mx_Field_5"><option value="org.matrix.msc3381.poll.disclosed">Open poll</option><option value="org.matrix.msc3381.poll.undisclosed">Closed poll</option></select><label for="mx_Field_5"></label></div><p>Voters see results as soon as they have voted</p><h2>What is your poll question or topic?</h2><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="poll-topic-input" maxlength="340" label="Question or topic" placeholder="Write something..." type="text" value="Poll Q"><label for="poll-topic-input">Question or topic</label></div><h2>Create options</h2><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_0" maxlength="340" label="Option 1" placeholder="Write an option" type="text" value="Answer 1"><label for="pollcreate_option_0">Option 1</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div class="mx_PollCreateDialog_option"><div class="mx_Field mx_Field_input mx_Field_labelAlwaysTopLeft mx_Field_placeholderIsHint"><input id="pollcreate_option_1" maxlength="340" label="Option 2" placeholder="Write an option" type="text" value="Answer 2"><label for="pollcreate_option_1">Option 2</label></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_removeOption"></div></div><div role="button" tabindex="0" class="mx_AccessibleButton mx_PollCreateDialog_addOption mx_AccessibleButton_hasKind mx_AccessibleButton_kind_secondary">Add option</div></div></div><div class="mx_CompoundDialog_footer"><div role="button" tabindex="0" class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline">Cancel</div><button type="submit" role="button" tabindex="0" class="mx_AccessibleButton mx_Dialog_nonDialogButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary">Done</button></div></form></div><div data-focus-guard="true" tabindex="0" style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"></div>"`;
|
||||||
|
|
|
@ -15,15 +15,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
|
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
|
||||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons";
|
import RoomHeaderButtons from "../../../../src/components/views/right_panel/RoomHeaderButtons";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
import { stubClient } from "../../../test-utils";
|
import { mkEvent, stubClient } from "../../../test-utils";
|
||||||
|
import { mkThread } from "../../../test-utils/threads";
|
||||||
|
|
||||||
describe("RoomHeaderButtons-test.tsx", function () {
|
describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
const ROOM_ID = "!roomId:example.org";
|
const ROOM_ID = "!roomId:example.org";
|
||||||
|
@ -35,6 +38,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
|
|
||||||
stubClient();
|
stubClient();
|
||||||
client = MatrixClientPeg.get();
|
client = MatrixClientPeg.get();
|
||||||
|
client.supportsExperimentalThreads = () => true;
|
||||||
room = new Room(ROOM_ID, client, client.getUserId() ?? "", {
|
room = new Room(ROOM_ID, client, client.getUserId() ?? "", {
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
});
|
});
|
||||||
|
@ -48,12 +52,12 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
return render(<RoomHeaderButtons room={room} excludedRightPanelPhaseButtons={[]} />);
|
return render(<RoomHeaderButtons room={room} excludedRightPanelPhaseButtons={[]} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getThreadButton(container) {
|
function getThreadButton(container: HTMLElement) {
|
||||||
return container.querySelector(".mx_RightPanel_threadsButton");
|
return container.querySelector(".mx_RightPanel_threadsButton");
|
||||||
}
|
}
|
||||||
|
|
||||||
function isIndicatorOfType(container, type: "red" | "gray") {
|
function isIndicatorOfType(container: HTMLElement, type: "red" | "gray" | "bold") {
|
||||||
return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator").className.includes(type);
|
return container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")!.className.includes(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
it("shows the thread button", () => {
|
it("shows the thread button", () => {
|
||||||
|
@ -76,7 +80,7 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("room wide notification does not change the thread button", () => {
|
it("thread notification does change the thread button", () => {
|
||||||
const { container } = getComponent(room);
|
const { container } = getComponent(room);
|
||||||
|
|
||||||
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
room.setThreadUnreadNotificationCount("$123", NotificationCountType.Total, 1);
|
||||||
|
@ -91,6 +95,85 @@ describe("RoomHeaderButtons-test.tsx", function () {
|
||||||
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("thread activity does change the thread button", async () => {
|
||||||
|
const { container } = getComponent(room);
|
||||||
|
|
||||||
|
// Thread activity should appear on the icon.
|
||||||
|
const { rootEvent, events } = mkThread({
|
||||||
|
room,
|
||||||
|
client,
|
||||||
|
authorId: client.getUserId()!,
|
||||||
|
participantUserIds: ["@alice:example.org"],
|
||||||
|
});
|
||||||
|
expect(isIndicatorOfType(container, "bold")).toBe(true);
|
||||||
|
|
||||||
|
// Sending the last event should clear the notification.
|
||||||
|
let event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.message",
|
||||||
|
user: client.getUserId()!,
|
||||||
|
room: room.roomId,
|
||||||
|
content: {
|
||||||
|
"msgtype": MsgType.Text,
|
||||||
|
"body": "Test",
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: rootEvent.getId(),
|
||||||
|
rel_type: RelationType.Thread,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
await expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||||
|
|
||||||
|
// Mark it as unread again.
|
||||||
|
event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.message",
|
||||||
|
user: "@alice:example.org",
|
||||||
|
room: room.roomId,
|
||||||
|
content: {
|
||||||
|
"msgtype": MsgType.Text,
|
||||||
|
"body": "Test",
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: rootEvent.getId(),
|
||||||
|
rel_type: RelationType.Thread,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
expect(isIndicatorOfType(container, "bold")).toBe(true);
|
||||||
|
|
||||||
|
// Sending a read receipt on an earlier event shouldn't do anything.
|
||||||
|
let receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: room.roomId,
|
||||||
|
content: {
|
||||||
|
[events.at(-1)!.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
expect(isIndicatorOfType(container, "bold")).toBe(true);
|
||||||
|
|
||||||
|
// Sending a receipt on the latest event should clear the notification.
|
||||||
|
receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: room.roomId,
|
||||||
|
content: {
|
||||||
|
[event.getId()!]: {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[client.getUserId()!]: { ts: 1, thread_id: rootEvent.getId() },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
expect(container.querySelector(".mx_RightPanel_threadsButton .mx_Indicator")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("does not explode without a room", () => {
|
it("does not explode without a room", () => {
|
||||||
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
client.canSupport.set(Feature.ThreadUnreadNotifications, ServerSupport.Unsupported);
|
||||||
expect(() => getComponent()).not.toThrow();
|
expect(() => getComponent()).not.toThrow();
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -141,9 +141,10 @@ describe("EventTile", () => {
|
||||||
mxEvent = rootEvent;
|
mxEvent = rootEvent;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows an unread notification bage", () => {
|
it("shows an unread notification badge", () => {
|
||||||
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
|
const { container } = getComponent({}, TimelineRenderingType.ThreadsList);
|
||||||
|
|
||||||
|
// By default, the thread will assume it is read.
|
||||||
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
expect(container.getElementsByClassName("mx_NotificationBadge")).toHaveLength(0);
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
|
|
|
@ -15,15 +15,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
// eslint-disable-next-line deprecate/import
|
import { EventType, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { mount, ReactWrapper } from "enzyme";
|
|
||||||
import { MatrixEvent, MsgType, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
||||||
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
|
||||||
|
import { act, render, screen } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../test-utils";
|
import {
|
||||||
import MessageComposer, {
|
createTestClient,
|
||||||
MessageComposer as MessageComposerClass,
|
filterConsole,
|
||||||
} from "../../../../src/components/views/rooms/MessageComposer";
|
flushPromises,
|
||||||
|
mkEvent,
|
||||||
|
mkStubRoom,
|
||||||
|
mockPlatformPeg,
|
||||||
|
stubClient,
|
||||||
|
} from "../../../test-utils";
|
||||||
|
import MessageComposer from "../../../../src/components/views/rooms/MessageComposer";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import RoomContext from "../../../../src/contexts/RoomContext";
|
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||||
|
@ -31,42 +37,108 @@ import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||||
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
||||||
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
|
||||||
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||||
import MessageComposerButtons from "../../../../src/components/views/rooms/MessageComposerButtons";
|
|
||||||
import { Features } from "../../../../src/settings/Settings";
|
import { Features } from "../../../../src/settings/Settings";
|
||||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
import dis from "../../../../src/dispatcher/dispatcher";
|
import dis from "../../../../src/dispatcher/dispatcher";
|
||||||
import { Action } from "../../../../src/dispatcher/actions";
|
|
||||||
import { SendMessageComposer } from "../../../../src/components/views/rooms/SendMessageComposer";
|
|
||||||
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
import { E2EStatus } from "../../../../src/utils/ShieldUtils";
|
||||||
import { addTextToComposerEnzyme } from "../../../test-utils/composer";
|
import { addTextToComposerRTL } from "../../../test-utils/composer";
|
||||||
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
|
import UIStore, { UI_EVENTS } from "../../../../src/stores/UIStore";
|
||||||
import { SendWysiwygComposer } from "../../../../src/components/views/rooms/wysiwyg_composer";
|
import { Action } from "../../../../src/dispatcher/actions";
|
||||||
|
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
|
||||||
|
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
|
||||||
|
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
|
||||||
|
import Modal from "../../../../src/Modal";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/components/views/rooms/wysiwyg_composer", () => ({
|
||||||
|
SendWysiwygComposer: jest.fn().mockImplementation(() => <div data-testid="wysiwyg-composer" />),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const openStickerPicker = async (): Promise<void> => {
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByLabelText("More options"));
|
||||||
|
await userEvent.click(screen.getByLabelText("Sticker"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const startVoiceMessage = async (): Promise<void> => {
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByLabelText("More options"));
|
||||||
|
await userEvent.click(screen.getByLabelText("Voice Message"));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
|
||||||
|
const recording = new VoiceBroadcastRecording(
|
||||||
|
mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"),
|
||||||
|
MatrixClientPeg.get(),
|
||||||
|
state,
|
||||||
|
);
|
||||||
|
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording);
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForModal = async (): Promise<void> => {
|
||||||
|
await flushPromises();
|
||||||
|
await flushPromises();
|
||||||
|
};
|
||||||
|
|
||||||
|
const shouldClearModal = async (): Promise<void> => {
|
||||||
|
afterEach(async () => {
|
||||||
|
Modal.closeCurrentModal("force");
|
||||||
|
await waitForModal();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectVoiceMessageRecordingTriggered = (): void => {
|
||||||
|
// Checking for the voice message dialog text, if no mic can be found.
|
||||||
|
// By this we know at least that starting a voice message was triggered.
|
||||||
|
expect(screen.getByText("No microphone found")).toBeInTheDocument();
|
||||||
|
};
|
||||||
|
|
||||||
describe("MessageComposer", () => {
|
describe("MessageComposer", () => {
|
||||||
stubClient();
|
stubClient();
|
||||||
const cli = createTestClient();
|
const cli = createTestClient();
|
||||||
|
|
||||||
|
filterConsole("Starting load of AsyncWrapper for modal");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockPlatformPeg();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.useRealTimers();
|
||||||
|
|
||||||
|
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
|
||||||
|
|
||||||
|
// restore settings
|
||||||
|
act(() => {
|
||||||
|
[
|
||||||
|
"MessageComposerInput.showStickersButton",
|
||||||
|
"MessageComposerInput.showPollsButton",
|
||||||
|
Features.VoiceBroadcast,
|
||||||
|
"feature_wysiwyg_composer",
|
||||||
|
].forEach((setting: string): void => {
|
||||||
|
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("for a Room", () => {
|
describe("for a Room", () => {
|
||||||
const room = mkStubRoom("!roomId:server", "Room 1", cli);
|
const room = mkStubRoom("!roomId:server", "Room 1", cli);
|
||||||
|
|
||||||
it("Renders a SendMessageComposer and MessageComposerButtons by default", () => {
|
it("Renders a SendMessageComposer and MessageComposerButtons by default", () => {
|
||||||
const wrapper = wrapAndRender({ room });
|
wrapAndRender({ room });
|
||||||
|
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
|
||||||
expect(wrapper.find("SendMessageComposer")).toHaveLength(1);
|
|
||||||
expect(wrapper.find("MessageComposerButtons")).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", () => {
|
it("Does not render a SendMessageComposer or MessageComposerButtons when user has no permission", () => {
|
||||||
const wrapper = wrapAndRender({ room }, false);
|
wrapAndRender({ room }, false);
|
||||||
|
expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument();
|
||||||
expect(wrapper.find("SendMessageComposer")).toHaveLength(0);
|
expect(screen.getByText("You do not have permission to post to this room")).toBeInTheDocument();
|
||||||
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
|
|
||||||
expect(wrapper.find(".mx_MessageComposer_noperm_error")).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => {
|
it("Does not render a SendMessageComposer or MessageComposerButtons when room is tombstoned", () => {
|
||||||
const wrapper = wrapAndRender(
|
wrapAndRender(
|
||||||
{ room },
|
{ room },
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
|
@ -81,13 +153,12 @@ describe("MessageComposer", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.find("SendMessageComposer")).toHaveLength(0);
|
expect(screen.queryByLabelText("Send a message…")).not.toBeInTheDocument();
|
||||||
expect(wrapper.find("MessageComposerButtons")).toHaveLength(0);
|
expect(screen.getByText("This room has been replaced and is no longer active.")).toBeInTheDocument();
|
||||||
expect(wrapper.find(".mx_MessageComposer_roomReplaced_header")).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when receiving a »reply_to_event«", () => {
|
describe("when receiving a »reply_to_event«", () => {
|
||||||
let wrapper: ReactWrapper;
|
let roomContext: IRoomState;
|
||||||
let resizeNotifier: ResizeNotifier;
|
let resizeNotifier: ResizeNotifier;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -95,18 +166,17 @@ describe("MessageComposer", () => {
|
||||||
resizeNotifier = {
|
resizeNotifier = {
|
||||||
notifyTimelineHeightChanged: jest.fn(),
|
notifyTimelineHeightChanged: jest.fn(),
|
||||||
} as unknown as ResizeNotifier;
|
} as unknown as ResizeNotifier;
|
||||||
wrapper = wrapAndRender({
|
roomContext = wrapAndRender({
|
||||||
room,
|
room,
|
||||||
resizeNotifier,
|
resizeNotifier,
|
||||||
});
|
}).roomContext;
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call notifyTimelineHeightChanged() for the same context", () => {
|
it("should call notifyTimelineHeightChanged() for the same context", () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "reply_to_event",
|
action: "reply_to_event",
|
||||||
context: (wrapper.instance as unknown as MessageComposerClass).context,
|
context: roomContext.timelineRenderingType,
|
||||||
});
|
});
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(150);
|
jest.advanceTimersByTime(150);
|
||||||
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled();
|
expect(resizeNotifier.notifyTimelineHeightChanged).toHaveBeenCalled();
|
||||||
|
@ -117,7 +187,6 @@ describe("MessageComposer", () => {
|
||||||
action: "reply_to_event",
|
action: "reply_to_event",
|
||||||
context: "test",
|
context: "test",
|
||||||
});
|
});
|
||||||
wrapper.update();
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(150);
|
jest.advanceTimersByTime(150);
|
||||||
expect(resizeNotifier.notifyTimelineHeightChanged).not.toHaveBeenCalled();
|
expect(resizeNotifier.notifyTimelineHeightChanged).not.toHaveBeenCalled();
|
||||||
|
@ -128,28 +197,33 @@ describe("MessageComposer", () => {
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
setting: "MessageComposerInput.showStickersButton",
|
setting: "MessageComposerInput.showStickersButton",
|
||||||
prop: "showStickersButton",
|
buttonLabel: "Sticker",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting: "MessageComposerInput.showPollsButton",
|
setting: "MessageComposerInput.showPollsButton",
|
||||||
prop: "showPollsButton",
|
buttonLabel: "Poll",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
setting: Features.VoiceBroadcast,
|
setting: Features.VoiceBroadcast,
|
||||||
prop: "showVoiceBroadcastButton",
|
buttonLabel: "Voice broadcast",
|
||||||
},
|
},
|
||||||
].forEach(({ setting, prop }) => {
|
].forEach(({ setting, buttonLabel }) => {
|
||||||
[true, false].forEach((value: boolean) => {
|
[true, false].forEach((value: boolean) => {
|
||||||
describe(`when ${setting} = ${value}`, () => {
|
describe(`when ${setting} = ${value}`, () => {
|
||||||
let wrapper: ReactWrapper;
|
beforeEach(async () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
|
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, value);
|
||||||
wrapper = wrapAndRender({ room });
|
wrapAndRender({ room });
|
||||||
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByLabelText("More options"));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should pass the prop ${prop} = ${value}`, () => {
|
it(`should${value || "not"} display the button`, () => {
|
||||||
expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(value);
|
if (value) {
|
||||||
|
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
|
||||||
|
} else {
|
||||||
|
expect(screen.queryByLabelText(buttonLabel)).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(`and setting ${setting} to ${!value}`, () => {
|
describe(`and setting ${setting} to ${!value}`, () => {
|
||||||
|
@ -164,11 +238,14 @@ describe("MessageComposer", () => {
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
wrapper.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should pass the prop ${prop} = ${!value}`, () => {
|
it(`should${!value || "not"} display the button`, () => {
|
||||||
expect(wrapper.find(MessageComposerButtons).props()[prop]).toBe(!value);
|
if (!value) {
|
||||||
|
expect(screen.getByLabelText(buttonLabel)).toBeInTheDocument();
|
||||||
|
} else {
|
||||||
|
expect(screen.queryByLabelText(buttonLabel)).not.toBeInTheDocument();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -176,26 +253,22 @@ describe("MessageComposer", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not render the send button", () => {
|
it("should not render the send button", () => {
|
||||||
const wrapper = wrapAndRender({ room });
|
wrapAndRender({ room });
|
||||||
expect(wrapper.find("SendButton")).toHaveLength(0);
|
expect(screen.queryByLabelText("Send message")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when a message has been entered", () => {
|
describe("when a message has been entered", () => {
|
||||||
let wrapper: ReactWrapper;
|
beforeEach(async () => {
|
||||||
|
const renderResult = wrapAndRender({ room }).renderResult;
|
||||||
beforeEach(() => {
|
await addTextToComposerRTL(renderResult, "Hello");
|
||||||
wrapper = wrapAndRender({ room });
|
|
||||||
addTextToComposerEnzyme(wrapper, "Hello");
|
|
||||||
wrapper.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the send button", () => {
|
it("should render the send button", () => {
|
||||||
expect(wrapper.find("SendButton")).toHaveLength(1);
|
expect(screen.getByLabelText("Send message")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("UIStore interactions", () => {
|
describe("UIStore interactions", () => {
|
||||||
let wrapper: ReactWrapper;
|
|
||||||
let resizeCallback: Function;
|
let resizeCallback: Function;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -205,74 +278,74 @@ describe("MessageComposer", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when a non-resize event occurred in UIStore", () => {
|
describe("when a non-resize event occurred in UIStore", () => {
|
||||||
let stateBefore: any;
|
beforeEach(async () => {
|
||||||
|
wrapAndRender({ room });
|
||||||
beforeEach(() => {
|
await openStickerPicker();
|
||||||
wrapper = wrapAndRender({ room }).children();
|
|
||||||
stateBefore = { ...wrapper.instance().state };
|
|
||||||
resizeCallback("test", {});
|
resizeCallback("test", {});
|
||||||
wrapper.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not change the state", () => {
|
it("should still display the sticker picker", () => {
|
||||||
expect(wrapper.instance().state).toEqual(stateBefore);
|
expect(screen.getByText("You don't currently have any stickerpacks enabled")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when a resize to narrow event occurred in UIStore", () => {
|
describe("when a resize to narrow event occurred in UIStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
wrapper = wrapAndRender({ room }, true, true).children();
|
wrapAndRender({ room }, true, true);
|
||||||
|
await openStickerPicker();
|
||||||
wrapper.setState({
|
|
||||||
isMenuOpen: true,
|
|
||||||
isStickerPickerOpen: true,
|
|
||||||
});
|
|
||||||
resizeCallback(UI_EVENTS.Resize, {});
|
resizeCallback(UI_EVENTS.Resize, {});
|
||||||
wrapper.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isMenuOpen should be true", () => {
|
it("should close the menu", () => {
|
||||||
expect(wrapper.state("isMenuOpen")).toBe(true);
|
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isStickerPickerOpen should be false", () => {
|
it("should not show the attachment button", () => {
|
||||||
expect(wrapper.state("isStickerPickerOpen")).toBe(false);
|
expect(screen.queryByLabelText("Attachment")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close the sticker picker", () => {
|
||||||
|
expect(
|
||||||
|
screen.queryByText("You don't currently have any stickerpacks enabled"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when a resize to non-narrow event occurred in UIStore", () => {
|
describe("when a resize to non-narrow event occurred in UIStore", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
wrapper = wrapAndRender({ room }, true, false).children();
|
wrapAndRender({ room }, true, false);
|
||||||
wrapper.setState({
|
await openStickerPicker();
|
||||||
isMenuOpen: true,
|
|
||||||
isStickerPickerOpen: true,
|
|
||||||
});
|
|
||||||
resizeCallback(UI_EVENTS.Resize, {});
|
resizeCallback(UI_EVENTS.Resize, {});
|
||||||
wrapper.update();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isMenuOpen should be false", () => {
|
it("should close the menu", () => {
|
||||||
expect(wrapper.state("isMenuOpen")).toBe(false);
|
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("isStickerPickerOpen should be false", () => {
|
it("should show the attachment button", () => {
|
||||||
expect(wrapper.state("isStickerPickerOpen")).toBe(false);
|
expect(screen.getByLabelText("Attachment")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should close the sticker picker", () => {
|
||||||
|
expect(
|
||||||
|
screen.queryByText("You don't currently have any stickerpacks enabled"),
|
||||||
|
).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when not replying to an event", () => {
|
describe("when not replying to an event", () => {
|
||||||
it("should pass the expected placeholder to SendMessageComposer", () => {
|
it("should pass the expected placeholder to SendMessageComposer", () => {
|
||||||
const wrapper = wrapAndRender({ room });
|
wrapAndRender({ room });
|
||||||
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send a message…");
|
expect(screen.getByLabelText("Send a message…")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => {
|
it("and an e2e status it should pass the expected placeholder to SendMessageComposer", () => {
|
||||||
const wrapper = wrapAndRender({
|
wrapAndRender({
|
||||||
room,
|
room,
|
||||||
e2eStatus: E2EStatus.Normal,
|
e2eStatus: E2EStatus.Normal,
|
||||||
});
|
});
|
||||||
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe("Send an encrypted message…");
|
expect(screen.getByLabelText("Send an encrypted message…")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -282,8 +355,8 @@ describe("MessageComposer", () => {
|
||||||
|
|
||||||
const checkPlaceholder = (expected: string) => {
|
const checkPlaceholder = (expected: string) => {
|
||||||
it("should pass the expected placeholder to SendMessageComposer", () => {
|
it("should pass the expected placeholder to SendMessageComposer", () => {
|
||||||
const wrapper = wrapAndRender(props);
|
wrapAndRender(props);
|
||||||
expect(wrapper.find(SendMessageComposer).props().placeholder).toBe(expected);
|
expect(screen.getByLabelText(expected)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -296,7 +369,7 @@ describe("MessageComposer", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
replyToEvent = mkEvent({
|
replyToEvent = mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: MsgType.Text,
|
type: EventType.RoomMessage,
|
||||||
user: cli.getUserId(),
|
user: cli.getUserId(),
|
||||||
content: {},
|
content: {},
|
||||||
});
|
});
|
||||||
|
@ -337,25 +410,72 @@ describe("MessageComposer", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when clicking start a voice message", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
wrapAndRender({ room });
|
||||||
|
await startVoiceMessage();
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldClearModal();
|
||||||
|
|
||||||
|
it("should try to start a voice message", () => {
|
||||||
|
expectVoiceMessageRecordingTriggered();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when recording a voice broadcast and trying to start a voice message", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
|
||||||
|
wrapAndRender({ room });
|
||||||
|
await startVoiceMessage();
|
||||||
|
await waitForModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldClearModal();
|
||||||
|
|
||||||
|
it("should not start a voice message and display the info dialog", async () => {
|
||||||
|
expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Can't start voice message")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
|
||||||
|
wrapAndRender({ room });
|
||||||
|
await startVoiceMessage();
|
||||||
|
await waitForModal();
|
||||||
|
});
|
||||||
|
|
||||||
|
shouldClearModal();
|
||||||
|
|
||||||
|
it("should try to start a voice message and should not display the info dialog", async () => {
|
||||||
|
expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument();
|
||||||
|
expectVoiceMessageRecordingTriggered();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("for a LocalRoom", () => {
|
describe("for a LocalRoom", () => {
|
||||||
const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!);
|
const localRoom = new LocalRoom("!room:example.com", cli, cli.getUserId()!);
|
||||||
|
|
||||||
it("should pass the sticker picker disabled prop", () => {
|
it("should not show the stickers button", async () => {
|
||||||
const wrapper = wrapAndRender({ room: localRoom });
|
wrapAndRender({ room: localRoom });
|
||||||
expect(wrapper.find(MessageComposerButtons).props().showStickersButton).toBe(false);
|
await act(async () => {
|
||||||
|
await userEvent.click(screen.getByLabelText("More options"));
|
||||||
|
});
|
||||||
|
expect(screen.queryByLabelText("Sticker")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render SendWysiwygComposer", () => {
|
it("should render SendWysiwygComposer when enabled", () => {
|
||||||
const room = mkStubRoom("!roomId:server", "Room 1", cli);
|
const room = mkStubRoom("!roomId:server", "Room 1", cli);
|
||||||
|
|
||||||
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
|
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, true);
|
||||||
const wrapper = wrapAndRender({ room });
|
|
||||||
|
|
||||||
SettingsStore.setValue("feature_wysiwyg_composer", null, SettingLevel.DEVICE, false);
|
wrapAndRender({ room });
|
||||||
expect(wrapper.find(SendWysiwygComposer)).toBeTruthy();
|
expect(screen.getByTestId("wysiwyg-composer")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -364,7 +484,7 @@ function wrapAndRender(
|
||||||
canSendMessages = true,
|
canSendMessages = true,
|
||||||
narrow = false,
|
narrow = false,
|
||||||
tombstone?: MatrixEvent,
|
tombstone?: MatrixEvent,
|
||||||
): ReactWrapper {
|
) {
|
||||||
const mockClient = MatrixClientPeg.get();
|
const mockClient = MatrixClientPeg.get();
|
||||||
const roomId = "myroomid";
|
const roomId = "myroomid";
|
||||||
const room: any = props.room || {
|
const room: any = props.room || {
|
||||||
|
@ -376,7 +496,7 @@ function wrapAndRender(
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const roomState = {
|
const roomContext = {
|
||||||
room,
|
room,
|
||||||
canSendMessages,
|
canSendMessages,
|
||||||
tombstone,
|
tombstone,
|
||||||
|
@ -389,11 +509,14 @@ function wrapAndRender(
|
||||||
permalinkCreator: new RoomPermalinkCreator(room),
|
permalinkCreator: new RoomPermalinkCreator(room),
|
||||||
};
|
};
|
||||||
|
|
||||||
return mount(
|
return {
|
||||||
|
renderResult: render(
|
||||||
<MatrixClientContext.Provider value={mockClient}>
|
<MatrixClientContext.Provider value={mockClient}>
|
||||||
<RoomContext.Provider value={roomState}>
|
<RoomContext.Provider value={roomContext}>
|
||||||
<MessageComposer {...defaultProps} {...props} />
|
<MessageComposer {...defaultProps} {...props} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
</MatrixClientContext.Provider>,
|
</MatrixClientContext.Provider>,
|
||||||
);
|
),
|
||||||
|
roomContext,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,15 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import "jest-mock";
|
import "jest-mock";
|
||||||
import { screen, act, render } from "@testing-library/react";
|
import { screen, act, render } from "@testing-library/react";
|
||||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
import { MatrixEvent, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||||
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
import { NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { mocked } from "jest-mock";
|
|
||||||
import { EventStatus } from "matrix-js-sdk/src/models/event-status";
|
import { EventStatus } from "matrix-js-sdk/src/models/event-status";
|
||||||
|
import { ReceiptType } from "matrix-js-sdk/src/@types/read_receipts";
|
||||||
|
|
||||||
|
import { mkThread } from "../../../../test-utils/threads";
|
||||||
import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
|
import { UnreadNotificationBadge } from "../../../../../src/components/views/rooms/NotificationBadge/UnreadNotificationBadge";
|
||||||
import { mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
import { mkEvent, mkMessage, stubClient } from "../../../../test-utils/test-utils";
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||||
import * as RoomNotifs from "../../../../../src/RoomNotifs";
|
import * as RoomNotifs from "../../../../../src/RoomNotifs";
|
||||||
|
|
||||||
|
@ -34,28 +36,57 @@ jest.mock("../../../../../src/RoomNotifs", () => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const ROOM_ID = "!roomId:example.org";
|
const ROOM_ID = "!roomId:example.org";
|
||||||
let THREAD_ID;
|
let THREAD_ID: string;
|
||||||
|
|
||||||
describe("UnreadNotificationBadge", () => {
|
describe("UnreadNotificationBadge", () => {
|
||||||
let mockClient: MatrixClient;
|
stubClient();
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
let room: Room;
|
let room: Room;
|
||||||
|
|
||||||
function getComponent(threadId?: string) {
|
function getComponent(threadId?: string) {
|
||||||
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
return <UnreadNotificationBadge room={room} threadId={threadId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
client.supportsExperimentalThreads = () => true;
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
|
||||||
stubClient();
|
room = new Room(ROOM_ID, client, client.getUserId()!, {
|
||||||
mockClient = mocked(MatrixClientPeg.get());
|
|
||||||
|
|
||||||
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
|
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const receipt = new MatrixEvent({
|
||||||
|
type: "m.receipt",
|
||||||
|
room_id: room.roomId,
|
||||||
|
content: {
|
||||||
|
"$event0:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[client.getUserId()!]: { ts: 1, thread_id: "$otherthread:localhost" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"$event1:localhost": {
|
||||||
|
[ReceiptType.Read]: {
|
||||||
|
[client.getUserId()!]: { ts: 1 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
room.addReceipt(receipt);
|
||||||
|
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
|
||||||
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
room.setUnreadNotificationCount(NotificationCountType.Highlight, 0);
|
||||||
|
|
||||||
|
const { rootEvent } = mkThread({
|
||||||
|
room,
|
||||||
|
client,
|
||||||
|
authorId: client.getUserId()!,
|
||||||
|
participantUserIds: [client.getUserId()!],
|
||||||
|
});
|
||||||
|
THREAD_ID = rootEvent.getId()!;
|
||||||
|
|
||||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 1);
|
||||||
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||||
|
|
||||||
|
@ -125,4 +156,34 @@ describe("UnreadNotificationBadge", () => {
|
||||||
const { container } = render(getComponent());
|
const { container } = render(getComponent());
|
||||||
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
expect(container.querySelector(".mx_NotificationBadge")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("activity renders unread notification badge", () => {
|
||||||
|
act(() => {
|
||||||
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Total, 0);
|
||||||
|
room.setThreadUnreadNotificationCount(THREAD_ID, NotificationCountType.Highlight, 0);
|
||||||
|
|
||||||
|
// Add another event on the thread which is not sent by us.
|
||||||
|
const event = mkEvent({
|
||||||
|
event: true,
|
||||||
|
type: "m.room.message",
|
||||||
|
user: "@alice:server.org",
|
||||||
|
room: room.roomId,
|
||||||
|
content: {
|
||||||
|
"msgtype": MsgType.Text,
|
||||||
|
"body": "Hello from Bob",
|
||||||
|
"m.relates_to": {
|
||||||
|
event_id: THREAD_ID,
|
||||||
|
rel_type: RelationType.Thread,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ts: 5,
|
||||||
|
});
|
||||||
|
room.addLiveEvents([event]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(getComponent(THREAD_ID));
|
||||||
|
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();
|
||||||
|
expect(container.querySelector(".mx_NotificationBadge_visible")).toBeTruthy();
|
||||||
|
expect(container.querySelector(".mx_NotificationBadge_highlighted")).toBeFalsy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,273 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
import ReactTestUtils from "react-dom/test-utils";
|
|
||||||
import ReactDOM from "react-dom";
|
|
||||||
import { PendingEventOrdering, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
import * as TestUtils from "../../../test-utils";
|
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
|
||||||
import dis from "../../../../src/dispatcher/dispatcher";
|
|
||||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
|
||||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
|
||||||
import RoomListStore, { RoomListStoreClass } from "../../../../src/stores/room-list/RoomListStore";
|
|
||||||
import RoomListLayoutStore from "../../../../src/stores/room-list/RoomListLayoutStore";
|
|
||||||
import RoomList from "../../../../src/components/views/rooms/RoomList";
|
|
||||||
import RoomSublist from "../../../../src/components/views/rooms/RoomSublist";
|
|
||||||
import { RoomTile } from "../../../../src/components/views/rooms/RoomTile";
|
|
||||||
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils";
|
|
||||||
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
|
|
||||||
|
|
||||||
function generateRoomId() {
|
|
||||||
return "!" + Math.random().toString().slice(2, 10) + ":domain";
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("RoomList", () => {
|
|
||||||
function createRoom(opts) {
|
|
||||||
const room = new Room(generateRoomId(), MatrixClientPeg.get(), client.getUserId(), {
|
|
||||||
// The room list now uses getPendingEvents(), so we need a detached ordering.
|
|
||||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
|
||||||
});
|
|
||||||
if (opts) {
|
|
||||||
Object.assign(room, opts);
|
|
||||||
}
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parentDiv = null;
|
|
||||||
let root = null;
|
|
||||||
const myUserId = "@me:domain";
|
|
||||||
|
|
||||||
const movingRoomId = "!someroomid";
|
|
||||||
let movingRoom: Room | undefined;
|
|
||||||
let otherRoom: Room | undefined;
|
|
||||||
|
|
||||||
let myMember: RoomMember | undefined;
|
|
||||||
let myOtherMember: RoomMember | undefined;
|
|
||||||
|
|
||||||
const client = getMockClientWithEventEmitter({
|
|
||||||
...mockClientMethodsUser(myUserId),
|
|
||||||
getRooms: jest.fn(),
|
|
||||||
getVisibleRooms: jest.fn(),
|
|
||||||
getRoom: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultProps = {
|
|
||||||
onKeyDown: jest.fn(),
|
|
||||||
onFocus: jest.fn(),
|
|
||||||
onBlur: jest.fn(),
|
|
||||||
onResize: jest.fn(),
|
|
||||||
resizeNotifier: {} as unknown as ResizeNotifier,
|
|
||||||
isMinimized: false,
|
|
||||||
activeSpace: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(async function (done) {
|
|
||||||
RoomListStoreClass.TEST_MODE = true;
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
client.credentials = { userId: myUserId };
|
|
||||||
|
|
||||||
DMRoomMap.makeShared();
|
|
||||||
|
|
||||||
parentDiv = document.createElement("div");
|
|
||||||
document.body.appendChild(parentDiv);
|
|
||||||
|
|
||||||
const WrappedRoomList = TestUtils.wrapInMatrixClientContext(RoomList);
|
|
||||||
root = ReactDOM.render(<WrappedRoomList {...defaultProps} />, parentDiv);
|
|
||||||
ReactTestUtils.findRenderedComponentWithType(root, RoomList);
|
|
||||||
|
|
||||||
movingRoom = createRoom({ name: "Moving room" });
|
|
||||||
expect(movingRoom.roomId).not.toBe(null);
|
|
||||||
|
|
||||||
// Mock joined member
|
|
||||||
myMember = new RoomMember(movingRoomId, myUserId);
|
|
||||||
myMember.membership = "join";
|
|
||||||
movingRoom.updateMyMembership("join");
|
|
||||||
movingRoom.getMember = (userId) =>
|
|
||||||
({
|
|
||||||
[client.credentials.userId]: myMember,
|
|
||||||
}[userId]);
|
|
||||||
|
|
||||||
otherRoom = createRoom({ name: "Other room" });
|
|
||||||
myOtherMember = new RoomMember(otherRoom.roomId, myUserId);
|
|
||||||
myOtherMember.membership = "join";
|
|
||||||
otherRoom.updateMyMembership("join");
|
|
||||||
otherRoom.getMember = (userId) =>
|
|
||||||
({
|
|
||||||
[client.credentials.userId]: myOtherMember,
|
|
||||||
}[userId]);
|
|
||||||
|
|
||||||
// Mock the matrix client
|
|
||||||
const mockRooms = [
|
|
||||||
movingRoom,
|
|
||||||
otherRoom,
|
|
||||||
createRoom({ tags: { "m.favourite": { order: 0.1 } }, name: "Some other room" }),
|
|
||||||
createRoom({ tags: { "m.favourite": { order: 0.2 } }, name: "Some other room 2" }),
|
|
||||||
createRoom({ tags: { "m.lowpriority": {} }, name: "Some unimportant room" }),
|
|
||||||
createRoom({ tags: { "custom.tag": {} }, name: "Some room customly tagged" }),
|
|
||||||
];
|
|
||||||
client.getRooms.mockReturnValue(mockRooms);
|
|
||||||
client.getVisibleRooms.mockReturnValue(mockRooms);
|
|
||||||
|
|
||||||
const roomMap = {};
|
|
||||||
client.getRooms().forEach((r) => {
|
|
||||||
roomMap[r.roomId] = r;
|
|
||||||
});
|
|
||||||
|
|
||||||
client.getRoom.mockImplementation((roomId) => roomMap[roomId]);
|
|
||||||
|
|
||||||
// Now that everything has been set up, prepare and update the store
|
|
||||||
await (RoomListStore.instance as RoomListStoreClass).makeReady(client);
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async (done) => {
|
|
||||||
if (parentDiv) {
|
|
||||||
ReactDOM.unmountComponentAtNode(parentDiv);
|
|
||||||
parentDiv.remove();
|
|
||||||
parentDiv = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await RoomListLayoutStore.instance.resetLayouts();
|
|
||||||
await (RoomListStore.instance as RoomListStoreClass).resetStore();
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
function expectRoomInSubList(room, subListTest) {
|
|
||||||
const subLists = ReactTestUtils.scryRenderedComponentsWithType(root, RoomSublist);
|
|
||||||
const containingSubList = subLists.find(subListTest);
|
|
||||||
|
|
||||||
let expectedRoomTile;
|
|
||||||
try {
|
|
||||||
const roomTiles = ReactTestUtils.scryRenderedComponentsWithType(containingSubList, RoomTile);
|
|
||||||
console.info({ roomTiles: roomTiles.length });
|
|
||||||
expectedRoomTile = roomTiles.find((tile) => tile.props.room === room);
|
|
||||||
} catch (err) {
|
|
||||||
// truncate the error message because it's spammy
|
|
||||||
err.message =
|
|
||||||
"Error finding RoomTile for " +
|
|
||||||
room.roomId +
|
|
||||||
" in " +
|
|
||||||
subListTest +
|
|
||||||
": " +
|
|
||||||
err.message.split("componentType")[0] +
|
|
||||||
"...";
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(expectedRoomTile).toBeTruthy();
|
|
||||||
expect(expectedRoomTile.props.room).toBe(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectCorrectMove(oldTagId, newTagId) {
|
|
||||||
const getTagSubListTest = (tagId) => {
|
|
||||||
return (s) => s.props.tagId === tagId;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Default to finding the destination sublist with newTag
|
|
||||||
const destSubListTest = getTagSubListTest(newTagId);
|
|
||||||
const srcSubListTest = getTagSubListTest(oldTagId);
|
|
||||||
|
|
||||||
// Set up the room that will be moved such that it has the correct state for a room in
|
|
||||||
// the section for oldTagId
|
|
||||||
if (oldTagId === DefaultTagID.Favourite || oldTagId === DefaultTagID.LowPriority) {
|
|
||||||
movingRoom.tags = { [oldTagId]: {} };
|
|
||||||
} else if (oldTagId === DefaultTagID.DM) {
|
|
||||||
// Mock inverse m.direct
|
|
||||||
// @ts-ignore forcing private property
|
|
||||||
DMRoomMap.shared().roomToUser = {
|
|
||||||
[movingRoom.roomId]: "@someotheruser:domain",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
dis.dispatch({ action: "MatrixActions.sync", prevState: null, state: "PREPARED", matrixClient: client });
|
|
||||||
|
|
||||||
expectRoomInSubList(movingRoom, srcSubListTest);
|
|
||||||
|
|
||||||
dis.dispatch({
|
|
||||||
action: "RoomListActions.tagRoom.pending",
|
|
||||||
request: {
|
|
||||||
oldTagId,
|
|
||||||
newTagId,
|
|
||||||
room: movingRoom,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expectRoomInSubList(movingRoom, destSubListTest);
|
|
||||||
}
|
|
||||||
|
|
||||||
function itDoesCorrectOptimisticUpdatesForDraggedRoomTiles() {
|
|
||||||
// TODO: Re-enable dragging tests when we support dragging again.
|
|
||||||
describe.skip("does correct optimistic update when dragging from", () => {
|
|
||||||
it("rooms to people", () => {
|
|
||||||
expectCorrectMove(undefined, DefaultTagID.DM);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rooms to favourites", () => {
|
|
||||||
expectCorrectMove(undefined, "m.favourite");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("rooms to low priority", () => {
|
|
||||||
expectCorrectMove(undefined, "m.lowpriority");
|
|
||||||
});
|
|
||||||
|
|
||||||
// XXX: Known to fail - the view does not update immediately to reflect the change.
|
|
||||||
// Whe running the app live, it updates when some other event occurs (likely the
|
|
||||||
// m.direct arriving) that these tests do not fire.
|
|
||||||
xit("people to rooms", () => {
|
|
||||||
expectCorrectMove(DefaultTagID.DM, undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("people to favourites", () => {
|
|
||||||
expectCorrectMove(DefaultTagID.DM, "m.favourite");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("people to lowpriority", () => {
|
|
||||||
expectCorrectMove(DefaultTagID.DM, "m.lowpriority");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("low priority to rooms", () => {
|
|
||||||
expectCorrectMove("m.lowpriority", undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("low priority to people", () => {
|
|
||||||
expectCorrectMove("m.lowpriority", DefaultTagID.DM);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("low priority to low priority", () => {
|
|
||||||
expectCorrectMove("m.lowpriority", "m.lowpriority");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("favourites to rooms", () => {
|
|
||||||
expectCorrectMove("m.favourite", undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("favourites to people", () => {
|
|
||||||
expectCorrectMove("m.favourite", DefaultTagID.DM);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("favourites to low priority", () => {
|
|
||||||
expectCorrectMove("m.favourite", "m.lowpriority");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
itDoesCorrectOptimisticUpdatesForDraggedRoomTiles();
|
|
||||||
});
|
|
|
@ -3,7 +3,7 @@
|
||||||
exports[`RoomTile should render the room 1`] = `
|
exports[`RoomTile should render the room 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
aria-label="!1:example.org Unread messages."
|
aria-label="!1:example.org"
|
||||||
aria-selected="false"
|
aria-selected="false"
|
||||||
class="mx_AccessibleButton mx_RoomTile"
|
class="mx_AccessibleButton mx_RoomTile"
|
||||||
role="treeitem"
|
role="treeitem"
|
||||||
|
@ -37,7 +37,7 @@ exports[`RoomTile should render the room 1`] = `
|
||||||
class="mx_RoomTile_titleContainer"
|
class="mx_RoomTile_titleContainer"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mx_RoomTile_title mx_RoomTile_titleHasUnreadEvents"
|
class="mx_RoomTile_title"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
title="!1:example.org"
|
title="!1:example.org"
|
||||||
>
|
>
|
||||||
|
@ -51,15 +51,7 @@ exports[`RoomTile should render the room 1`] = `
|
||||||
<div
|
<div
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
class="mx_RoomTile_badgeContainer"
|
class="mx_RoomTile_badgeContainer"
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_dot"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="mx_NotificationBadge_count"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
|
|
|
@ -27,6 +27,8 @@ import { SubSelection } from "../../../../../../src/components/views/rooms/wysiw
|
||||||
describe("LinkModal", () => {
|
describe("LinkModal", () => {
|
||||||
const formattingFunctions = {
|
const formattingFunctions = {
|
||||||
link: jest.fn(),
|
link: jest.fn(),
|
||||||
|
removeLinks: jest.fn(),
|
||||||
|
getLink: jest.fn().mockReturnValue("my initial content"),
|
||||||
} as unknown as FormattingFunctions;
|
} as unknown as FormattingFunctions;
|
||||||
const defaultValue: SubSelection = {
|
const defaultValue: SubSelection = {
|
||||||
focusNode: null,
|
focusNode: null,
|
||||||
|
@ -35,13 +37,14 @@ describe("LinkModal", () => {
|
||||||
anchorOffset: 4,
|
anchorOffset: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customRender = (isTextEnabled: boolean, onClose: () => void) => {
|
const customRender = (isTextEnabled: boolean, onClose: () => void, isEditing = false) => {
|
||||||
return render(
|
return render(
|
||||||
<LinkModal
|
<LinkModal
|
||||||
composer={formattingFunctions}
|
composer={formattingFunctions}
|
||||||
isTextEnabled={isTextEnabled}
|
isTextEnabled={isTextEnabled}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
composerContext={{ selection: defaultValue }}
|
composerContext={{ selection: defaultValue }}
|
||||||
|
isEditing={isEditing}
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -75,13 +78,13 @@ describe("LinkModal", () => {
|
||||||
// When
|
// When
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
screen.getByText("Save").click();
|
screen.getByText("Save").click();
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
|
await waitFor(() => {
|
||||||
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
||||||
await waitFor(() => expect(onClose).toBeCalledTimes(1));
|
expect(onClose).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
// When
|
|
||||||
jest.runAllTimers();
|
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(formattingFunctions.link).toHaveBeenCalledWith("l", undefined);
|
expect(formattingFunctions.link).toHaveBeenCalledWith("l", undefined);
|
||||||
|
@ -118,15 +121,41 @@ describe("LinkModal", () => {
|
||||||
// When
|
// When
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
screen.getByText("Save").click();
|
screen.getByText("Save").click();
|
||||||
|
jest.runAllTimers();
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
|
await waitFor(() => {
|
||||||
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
expect(selectionSpy).toHaveBeenCalledWith(defaultValue);
|
||||||
await waitFor(() => expect(onClose).toBeCalledTimes(1));
|
expect(onClose).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
// When
|
|
||||||
jest.runAllTimers();
|
|
||||||
|
|
||||||
// Then
|
// Then
|
||||||
expect(formattingFunctions.link).toHaveBeenCalledWith("l", "t");
|
expect(formattingFunctions.link).toHaveBeenCalledWith("l", "t");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should remove the link", async () => {
|
||||||
|
// When
|
||||||
|
const onClose = jest.fn();
|
||||||
|
customRender(true, onClose, true);
|
||||||
|
await userEvent.click(screen.getByText("Remove"));
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(formattingFunctions.removeLinks).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onClose).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should display the link in editing", async () => {
|
||||||
|
// When
|
||||||
|
customRender(true, jest.fn(), true);
|
||||||
|
|
||||||
|
// Then
|
||||||
|
expect(screen.getByLabelText("Link")).toContainHTML("my initial content");
|
||||||
|
expect(screen.getByText("Save")).toBeDisabled();
|
||||||
|
|
||||||
|
// When
|
||||||
|
await userEvent.type(screen.getByLabelText("Link"), "l");
|
||||||
|
|
||||||
|
// Then
|
||||||
|
await waitFor(() => expect(screen.getByText("Save")).toBeEnabled());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -46,6 +46,7 @@ import LogoutDialog from "../../../../../../src/components/views/dialogs/LogoutD
|
||||||
import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
|
import { DeviceSecurityVariation, ExtendedDevice } from "../../../../../../src/components/views/settings/devices/types";
|
||||||
import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter";
|
import { INACTIVE_DEVICE_AGE_MS } from "../../../../../../src/components/views/settings/devices/filter";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
|
import { getClientInformationEventType } from "../../../../../../src/utils/device/clientInformation";
|
||||||
|
|
||||||
mockPlatformPeg();
|
mockPlatformPeg();
|
||||||
|
|
||||||
|
@ -87,6 +88,7 @@ describe("<SessionManagerTab />", () => {
|
||||||
generateClientSecret: jest.fn(),
|
generateClientSecret: jest.fn(),
|
||||||
setDeviceDetails: jest.fn(),
|
setDeviceDetails: jest.fn(),
|
||||||
getAccountData: jest.fn(),
|
getAccountData: jest.fn(),
|
||||||
|
deleteAccountData: jest.fn(),
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(true),
|
||||||
getPushers: jest.fn(),
|
getPushers: jest.fn(),
|
||||||
setPusher: jest.fn(),
|
setPusher: jest.fn(),
|
||||||
|
@ -182,6 +184,9 @@ describe("<SessionManagerTab />", () => {
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// @ts-ignore mock
|
||||||
|
mockClient.store = { accountData: {} };
|
||||||
|
|
||||||
mockClient.getAccountData.mockReset().mockImplementation((eventType) => {
|
mockClient.getAccountData.mockReset().mockImplementation((eventType) => {
|
||||||
if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
|
if (eventType.startsWith(LOCAL_NOTIFICATION_SETTINGS_PREFIX.name)) {
|
||||||
return new MatrixEvent({
|
return new MatrixEvent({
|
||||||
|
@ -667,6 +672,47 @@ describe("<SessionManagerTab />", () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("removes account data events for devices after sign out", async () => {
|
||||||
|
const mobileDeviceClientInfo = new MatrixEvent({
|
||||||
|
type: getClientInformationEventType(alicesMobileDevice.device_id),
|
||||||
|
content: {
|
||||||
|
name: "test",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
// @ts-ignore setup mock
|
||||||
|
mockClient.store = {
|
||||||
|
// @ts-ignore setup mock
|
||||||
|
accountData: {
|
||||||
|
[mobileDeviceClientInfo.getType()]: mobileDeviceClientInfo,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
mockClient.getDevices
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
devices: [alicesDevice, alicesMobileDevice, alicesOlderMobileDevice],
|
||||||
|
})
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
// refreshed devices after sign out
|
||||||
|
devices: [alicesDevice],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getByTestId, getByLabelText } = render(getComponent());
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await flushPromises();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockClient.deleteAccountData).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
fireEvent.click(getByTestId("current-session-menu"));
|
||||||
|
fireEvent.click(getByLabelText("Sign out of all other sessions (2)"));
|
||||||
|
await confirmSignout(getByTestId);
|
||||||
|
|
||||||
|
// only called once for signed out device with account data event
|
||||||
|
expect(mockClient.deleteAccountData).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockClient.deleteAccountData).toHaveBeenCalledWith(mobileDeviceClientInfo.getType());
|
||||||
|
});
|
||||||
|
|
||||||
describe("other devices", () => {
|
describe("other devices", () => {
|
||||||
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } };
|
const interactiveAuthError = { httpStatus: 401, data: { flows: [{ stages: ["m.login.password"] }] } };
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,8 @@ limitations under the License.
|
||||||
// eslint-disable-next-line deprecate/import
|
// eslint-disable-next-line deprecate/import
|
||||||
import { ReactWrapper } from "enzyme";
|
import { ReactWrapper } from "enzyme";
|
||||||
import { act } from "react-dom/test-utils";
|
import { act } from "react-dom/test-utils";
|
||||||
import { fireEvent } from "@testing-library/react";
|
import { act as actRTL, fireEvent, RenderResult } from "@testing-library/react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
export const addTextToComposer = (container: HTMLElement, text: string) =>
|
export const addTextToComposer = (container: HTMLElement, text: string) =>
|
||||||
act(() => {
|
act(() => {
|
||||||
|
@ -47,3 +48,10 @@ export const addTextToComposerEnzyme = (wrapper: ReactWrapper, text: string) =>
|
||||||
wrapper.find('[role="textbox"]').simulate("paste", pasteEvent);
|
wrapper.find('[role="textbox"]').simulate("paste", pasteEvent);
|
||||||
wrapper.update();
|
wrapper.update();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const addTextToComposerRTL = async (renderResult: RenderResult, text: string): Promise<void> => {
|
||||||
|
await actRTL(async () => {
|
||||||
|
await userEvent.click(renderResult.getByLabelText("Send a message…"));
|
||||||
|
await userEvent.keyboard(text);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
42
test/test-utils/events.ts
Normal file
42
test/test-utils/events.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MsgType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
interface MessageContent {
|
||||||
|
msgtype: MsgType;
|
||||||
|
body: string;
|
||||||
|
format?: string;
|
||||||
|
formatted_body?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the `content` for an `m.room.message` event based on input.
|
||||||
|
* @param text The text to put in the event.
|
||||||
|
* @param html Optional HTML to put in the event.
|
||||||
|
* @returns A complete `content` object for an `m.room.message` event.
|
||||||
|
*/
|
||||||
|
export function createMessageEventContent(text: string, html?: string): MessageContent {
|
||||||
|
const content: MessageContent = {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: text,
|
||||||
|
};
|
||||||
|
if (html) {
|
||||||
|
content.format = "org.matrix.custom.html";
|
||||||
|
content.formatted_body = html;
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
|
@ -254,12 +254,12 @@ describe("VoiceBroadcastRecording", () => {
|
||||||
expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Started);
|
expect(voiceBroadcastRecording.getState()).toBe(VoiceBroadcastInfoState.Started);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and calling stop()", () => {
|
describe("and calling stop", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
voiceBroadcastRecording.stop();
|
voiceBroadcastRecording.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 1);
|
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 0);
|
||||||
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
|
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
|
||||||
|
|
||||||
it("should emit a stopped state changed event", () => {
|
it("should emit a stopped state changed event", () => {
|
||||||
|
@ -351,6 +351,7 @@ describe("VoiceBroadcastRecording", () => {
|
||||||
|
|
||||||
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
|
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
|
||||||
itShouldSendAVoiceMessage([23, 24, 25], 3, getMaxBroadcastLength(), 2);
|
itShouldSendAVoiceMessage([23, 24, 25], 3, getMaxBroadcastLength(), 2);
|
||||||
|
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -364,6 +365,7 @@ describe("VoiceBroadcastRecording", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldSendAVoiceMessage([4, 5, 6], 3, 42, 1);
|
itShouldSendAVoiceMessage([4, 5, 6], 3, 42, 1);
|
||||||
|
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Stopped, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe.each([
|
describe.each([
|
||||||
|
@ -375,7 +377,7 @@ describe("VoiceBroadcastRecording", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldBeInState(VoiceBroadcastInfoState.Paused);
|
itShouldBeInState(VoiceBroadcastInfoState.Paused);
|
||||||
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused, 1);
|
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused, 0);
|
||||||
|
|
||||||
it("should stop the recorder", () => {
|
it("should stop the recorder", () => {
|
||||||
expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
|
expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
|
||||||
|
@ -413,7 +415,7 @@ describe("VoiceBroadcastRecording", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldBeInState(VoiceBroadcastInfoState.Resumed);
|
itShouldBeInState(VoiceBroadcastInfoState.Resumed);
|
||||||
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Resumed, 1);
|
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Resumed, 0);
|
||||||
|
|
||||||
it("should start the recorder", () => {
|
it("should start the recorder", () => {
|
||||||
expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled();
|
expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled();
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`setUpVoiceBroadcastPreRecording when trying to start a broadcast if there is no connection should show an info dialog and not set up a pre-recording 1`] = `
|
||||||
|
[MockFunction] {
|
||||||
|
"calls": [
|
||||||
|
[
|
||||||
|
[Function],
|
||||||
|
{
|
||||||
|
"description": <p>
|
||||||
|
Unfortunately we're unable to start a recording right now. Please try again later.
|
||||||
|
</p>,
|
||||||
|
"hasCloseButton": true,
|
||||||
|
"title": "Connection error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"type": "return",
|
||||||
|
"value": undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
|
@ -91,3 +91,26 @@ exports[`startNewVoiceBroadcastRecording when the current user is not allowed to
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`startNewVoiceBroadcastRecording when trying to start a broadcast if there is no connection should show an info dialog and not start a recording 1`] = `
|
||||||
|
[MockFunction] {
|
||||||
|
"calls": [
|
||||||
|
[
|
||||||
|
[Function],
|
||||||
|
{
|
||||||
|
"description": <p>
|
||||||
|
Unfortunately we're unable to start a recording right now. Please try again later.
|
||||||
|
</p>,
|
||||||
|
"hasCloseButton": true,
|
||||||
|
"title": "Connection error",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"type": "return",
|
||||||
|
"value": undefined,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
|
@ -16,9 +16,10 @@ limitations under the License.
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
|
import Modal from "../../../src/Modal";
|
||||||
import {
|
import {
|
||||||
checkVoiceBroadcastPreConditions,
|
|
||||||
VoiceBroadcastInfoState,
|
VoiceBroadcastInfoState,
|
||||||
VoiceBroadcastPlayback,
|
VoiceBroadcastPlayback,
|
||||||
VoiceBroadcastPlaybacksStore,
|
VoiceBroadcastPlaybacksStore,
|
||||||
|
@ -30,7 +31,7 @@ import { setUpVoiceBroadcastPreRecording } from "../../../src/voice-broadcast/ut
|
||||||
import { mkRoomMemberJoinEvent, stubClient } from "../../test-utils";
|
import { mkRoomMemberJoinEvent, stubClient } from "../../test-utils";
|
||||||
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
|
||||||
|
|
||||||
jest.mock("../../../src/voice-broadcast/utils/checkVoiceBroadcastPreConditions");
|
jest.mock("../../../src/Modal");
|
||||||
|
|
||||||
describe("setUpVoiceBroadcastPreRecording", () => {
|
describe("setUpVoiceBroadcastPreRecording", () => {
|
||||||
const roomId = "!room:example.com";
|
const roomId = "!room:example.com";
|
||||||
|
@ -86,20 +87,19 @@ describe("setUpVoiceBroadcastPreRecording", () => {
|
||||||
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
playbacksStore = new VoiceBroadcastPlaybacksStore(recordingsStore);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when the preconditions fail", () => {
|
describe("when trying to start a broadcast if there is no connection", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mocked(checkVoiceBroadcastPreConditions).mockResolvedValue(false);
|
mocked(client.getSyncState).mockReturnValue(SyncState.Error);
|
||||||
await setUpPreRecording();
|
await setUpPreRecording();
|
||||||
});
|
});
|
||||||
|
|
||||||
itShouldNotCreateAPreRecording();
|
it("should show an info dialog and not set up a pre-recording", () => {
|
||||||
});
|
expect(preRecordingStore.getCurrent()).toBeNull();
|
||||||
|
expect(Modal.createDialog).toMatchSnapshot();
|
||||||
describe("when the preconditions pass", () => {
|
});
|
||||||
beforeEach(() => {
|
|
||||||
mocked(checkVoiceBroadcastPreConditions).mockResolvedValue(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when setting up a pre-recording", () => {
|
||||||
describe("and there is no user id", () => {
|
describe("and there is no user id", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
mocked(client.getUserId).mockReturnValue(null);
|
mocked(client.getUserId).mockReturnValue(null);
|
||||||
|
@ -120,17 +120,15 @@ describe("setUpVoiceBroadcastPreRecording", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("and there is a room member and listening to another broadcast", () => {
|
describe("and there is a room member and listening to another broadcast", () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
playbacksStore.setCurrent(playback);
|
playbacksStore.setCurrent(playback);
|
||||||
room.currentState.setStateEvents([mkRoomMemberJoinEvent(userId, roomId)]);
|
room.currentState.setStateEvents([mkRoomMemberJoinEvent(userId, roomId)]);
|
||||||
setUpPreRecording();
|
await setUpPreRecording();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pause the current playback and create a voice broadcast pre-recording", () => {
|
it("should pause the current playback and create a voice broadcast pre-recording", () => {
|
||||||
expect(playback.pause).toHaveBeenCalled();
|
expect(playback.pause).toHaveBeenCalled();
|
||||||
expect(playbacksStore.getCurrent()).toBeNull();
|
expect(playbacksStore.getCurrent()).toBeNull();
|
||||||
|
|
||||||
expect(checkVoiceBroadcastPreConditions).toHaveBeenCalledWith(room, client, recordingsStore);
|
|
||||||
expect(preRecording).toBeInstanceOf(VoiceBroadcastPreRecording);
|
expect(preRecording).toBeInstanceOf(VoiceBroadcastPreRecording);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
import { EventType, ISendEventResponse, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { SyncState } from "matrix-js-sdk/src/sync";
|
||||||
|
|
||||||
import Modal from "../../../src/Modal";
|
import Modal from "../../../src/Modal";
|
||||||
import {
|
import {
|
||||||
|
@ -103,6 +104,18 @@ describe("startNewVoiceBroadcastRecording", () => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when trying to start a broadcast if there is no connection", () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
mocked(client.getSyncState).mockReturnValue(SyncState.Error);
|
||||||
|
result = await startNewVoiceBroadcastRecording(room, client, playbacksStore, recordingsStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show an info dialog and not start a recording", () => {
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(Modal.createDialog).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when the current user is allowed to send voice broadcast info state events", () => {
|
describe("when the current user is allowed to send voice broadcast info state events", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mocked(room.currentState.maySendStateEvent).mockReturnValue(true);
|
mocked(room.currentState.maySendStateEvent).mockReturnValue(true);
|
||||||
|
|
|
@ -1525,10 +1525,10 @@
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-sdk-crypto-js/-/matrix-sdk-crypto-js-0.1.0-alpha.2.tgz#a09d0fea858e817da971a3c9f904632ef7b49eb6"
|
||||||
integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA==
|
integrity sha512-oVkBCh9YP7H9i4gAoQbZzswniczfo/aIptNa4dxRi4Ff9lSvUCFv6Hvzi7C+90c0/PWZLXjIDTIAWZYmwyd2fA==
|
||||||
|
|
||||||
"@matrix-org/matrix-wysiwyg@^0.13.0":
|
"@matrix-org/matrix-wysiwyg@^0.14.0":
|
||||||
version "0.13.0"
|
version "0.14.0"
|
||||||
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.13.0.tgz#e643df4e13cdc5dbf9285740bc0ce2aef9873c16"
|
resolved "https://registry.yarnpkg.com/@matrix-org/matrix-wysiwyg/-/matrix-wysiwyg-0.14.0.tgz#359fabf5af403b3f128fe6ede3bff9754a9e18c4"
|
||||||
integrity sha512-MCeTj4hkl0snjlygd1v+mEEOgaN6agyjAVjJEbvEvP/BaYaDiPEXMTDaRQrcUt3OIY53UNhm1DDEn4yPTn83Jg==
|
integrity sha512-iSwIR7kS/zwAzy/8S5cUMv2aceoJl/vIGhqmY9hSU0gVyzmsyaVnx00uNMvVDBUFiiPT2gonN8R3+dxg58TPaQ==
|
||||||
|
|
||||||
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
"@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.14.tgz":
|
||||||
version "3.2.14"
|
version "3.2.14"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue