Merge branch 'develop' of https://github.com/yaya-usman/matrix-react-sdk into favouriteMessages_Panel
commit
a53f7f8302
|
@ -22,7 +22,7 @@ jobs:
|
||||||
|
|
||||||
- id: prdetails
|
- id: prdetails
|
||||||
if: github.event.workflow_run.event == 'pull_request'
|
if: github.event.workflow_run.event == 'pull_request'
|
||||||
uses: matrix-org/pr-details-action@v1.1
|
uses: matrix-org/pr-details-action@v1.2
|
||||||
with:
|
with:
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
branch: ${{ github.event.workflow_run.head_branch }}
|
branch: ${{ github.event.workflow_run.head_branch }}
|
||||||
|
|
|
@ -25,7 +25,7 @@ jobs:
|
||||||
Exercise caution. Use test accounts.
|
Exercise caution. Use test accounts.
|
||||||
|
|
||||||
- id: prdetails
|
- id: prdetails
|
||||||
uses: matrix-org/pr-details-action@v1.1
|
uses: matrix-org/pr-details-action@v1.2
|
||||||
with:
|
with:
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
||||||
branch: ${{ github.event.workflow_run.head_branch }}
|
branch: ${{ github.event.workflow_run.head_branch }}
|
||||||
|
|
|
@ -139,6 +139,9 @@ describe("Spotlight", () => {
|
||||||
const room2Name = "Lounge";
|
const room2Name = "Lounge";
|
||||||
let room2Id: string;
|
let room2Id: string;
|
||||||
|
|
||||||
|
const room3Name = "Public";
|
||||||
|
let room3Id: string;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.startSynapse("default").then(data => {
|
cy.startSynapse("default").then(data => {
|
||||||
synapse = data;
|
synapse = data;
|
||||||
|
@ -163,6 +166,19 @@ describe("Spotlight", () => {
|
||||||
room2Id = _room2Id;
|
room2Id = _room2Id;
|
||||||
bot2.invite(room2Id, bot1.getUserId());
|
bot2.invite(room2Id, bot1.getUserId());
|
||||||
});
|
});
|
||||||
|
bot2.createRoom({
|
||||||
|
name: room3Name,
|
||||||
|
visibility: Visibility.Public, initial_state: [{
|
||||||
|
type: "m.room.history_visibility",
|
||||||
|
state_key: "",
|
||||||
|
content: {
|
||||||
|
history_visibility: "world_readable",
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
}).then(({ room_id: _room3Id }) => {
|
||||||
|
room3Id = _room3Id;
|
||||||
|
bot2.invite(room3Id, bot1.getUserId());
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
).then(() =>
|
).then(() =>
|
||||||
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
|
cy.get('.mx_RoomSublist_skeletonUI').should('not.exist'),
|
||||||
|
@ -212,6 +228,7 @@ describe("Spotlight", () => {
|
||||||
cy.spotlightSearch().clear().type(room1Name);
|
cy.spotlightSearch().clear().type(room1Name);
|
||||||
cy.spotlightResults().should("have.length", 1);
|
cy.spotlightResults().should("have.length", 1);
|
||||||
cy.spotlightResults().eq(0).should("contain", room1Name);
|
cy.spotlightResults().eq(0).should("contain", room1Name);
|
||||||
|
cy.spotlightResults().eq(0).should("contain", "View");
|
||||||
cy.spotlightResults().eq(0).click();
|
cy.spotlightResults().eq(0).click();
|
||||||
cy.url().should("contain", room1Id);
|
cy.url().should("contain", room1Id);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -225,6 +242,7 @@ describe("Spotlight", () => {
|
||||||
cy.spotlightSearch().clear().type(room2Name);
|
cy.spotlightSearch().clear().type(room2Name);
|
||||||
cy.spotlightResults().should("have.length", 1);
|
cy.spotlightResults().should("have.length", 1);
|
||||||
cy.spotlightResults().eq(0).should("contain", room2Name);
|
cy.spotlightResults().eq(0).should("contain", room2Name);
|
||||||
|
cy.spotlightResults().eq(0).should("contain", "Join");
|
||||||
cy.spotlightResults().eq(0).click();
|
cy.spotlightResults().eq(0).click();
|
||||||
cy.url().should("contain", room2Id);
|
cy.url().should("contain", room2Id);
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
|
@ -233,6 +251,21 @@ describe("Spotlight", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should find unknown public world readable rooms", () => {
|
||||||
|
cy.openSpotlightDialog().within(() => {
|
||||||
|
cy.spotlightFilter(Filter.PublicRooms);
|
||||||
|
cy.spotlightSearch().clear().type(room3Name);
|
||||||
|
cy.spotlightResults().should("have.length", 1);
|
||||||
|
cy.spotlightResults().eq(0).should("contain", room3Name);
|
||||||
|
cy.spotlightResults().eq(0).should("contain", "View");
|
||||||
|
cy.spotlightResults().eq(0).click();
|
||||||
|
cy.url().should("contain", room3Id);
|
||||||
|
}).then(() => {
|
||||||
|
cy.get(".mx_RoomPreviewBar_actions .mx_AccessibleButton").click();
|
||||||
|
cy.roomHeaderName().should("contain", room3Name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
// TODO: We currently can’t test finding rooms on other homeservers/other protocols
|
||||||
// We obviously don’t have federation or bridges in cypress tests
|
// We obviously don’t have federation or bridges in cypress tests
|
||||||
/*
|
/*
|
||||||
|
|
|
@ -0,0 +1,249 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import { PollResponseEvent } from "matrix-events-sdk";
|
||||||
|
|
||||||
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
|
import { MatrixClient } from "../../global";
|
||||||
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
const hideTimestampCSS = ".mx_MessageTimestamp { visibility: hidden !important; }";
|
||||||
|
|
||||||
|
describe("Polls", () => {
|
||||||
|
let synapse: SynapseInstance;
|
||||||
|
|
||||||
|
type CreatePollOptions = {
|
||||||
|
title: string;
|
||||||
|
options: string[];
|
||||||
|
};
|
||||||
|
const createPoll = ({ title, options }: CreatePollOptions) => {
|
||||||
|
if (options.length < 2) {
|
||||||
|
throw new Error('Poll must have at least two options');
|
||||||
|
}
|
||||||
|
cy.get('.mx_PollCreateDialog').within((pollCreateDialog) => {
|
||||||
|
cy.get('#poll-topic-input').type(title);
|
||||||
|
|
||||||
|
options.forEach((option, index) => {
|
||||||
|
const optionId = `#pollcreate_option_${index}`;
|
||||||
|
|
||||||
|
// click 'add option' button if needed
|
||||||
|
if (pollCreateDialog.find(optionId).length === 0) {
|
||||||
|
cy.get('.mx_PollCreateDialog_addOption').scrollIntoView().click();
|
||||||
|
}
|
||||||
|
cy.get(optionId).scrollIntoView().type(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
cy.get('.mx_Dialog button[type="submit"]').click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPollTile = (pollId: string): Chainable<JQuery> => {
|
||||||
|
return cy.get(`.mx_EventTile[data-scroll-tokens="${pollId}"]`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPollOption = (pollId: string, optionText: string): Chainable<JQuery> => {
|
||||||
|
return getPollTile(pollId).contains('.mx_MPollBody_option .mx_StyledRadioButton', optionText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const expectPollOptionVoteCount = (pollId: string, optionText: string, votes: number): void => {
|
||||||
|
getPollOption(pollId, optionText).within(() => {
|
||||||
|
cy.get('.mx_MPollBody_optionVoteCount').should('contain', `${votes} vote`);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const botVoteForOption = (bot: MatrixClient, roomId: string, pollId: string, optionText: string): void => {
|
||||||
|
getPollOption(pollId, optionText).within(ref => {
|
||||||
|
cy.get('input[type="radio"]').invoke('attr', 'value').then(optionId => {
|
||||||
|
const pollVote = PollResponseEvent.from([optionId], pollId).serialize();
|
||||||
|
bot.sendEvent(
|
||||||
|
roomId,
|
||||||
|
pollVote.type,
|
||||||
|
pollVote.content,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.enableLabsFeature("feature_thread");
|
||||||
|
cy.window().then(win => {
|
||||||
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
|
});
|
||||||
|
cy.startSynapse("default").then(data => {
|
||||||
|
synapse = data;
|
||||||
|
|
||||||
|
cy.initTestUser(synapse, "Tom");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.stopSynapse(synapse);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Open polls can be created and voted in", () => {
|
||||||
|
let bot: MatrixClient;
|
||||||
|
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||||
|
bot = _bot;
|
||||||
|
});
|
||||||
|
|
||||||
|
let roomId: string;
|
||||||
|
cy.createRoom({}).then(_roomId => {
|
||||||
|
roomId = _roomId;
|
||||||
|
cy.inviteUser(roomId, bot.getUserId());
|
||||||
|
cy.visit('/#/room/' + roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.openMessageComposerOptions().within(() => {
|
||||||
|
cy.get('[aria-label="Poll"]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.mx_CompoundDialog').percySnapshotElement('Polls Composer');
|
||||||
|
|
||||||
|
const pollParams = {
|
||||||
|
title: 'Does the polls feature work?',
|
||||||
|
options: ['Yes', 'No', 'Maybe'],
|
||||||
|
};
|
||||||
|
createPoll(pollParams);
|
||||||
|
|
||||||
|
// Wait for message to send, get its ID and save as @pollId
|
||||||
|
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||||
|
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||||
|
|
||||||
|
cy.get<string>("@pollId").then(pollId => {
|
||||||
|
getPollTile(pollId).percySnapshotElement('Polls Timeline tile - no votes', { percyCSS: hideTimestampCSS });
|
||||||
|
|
||||||
|
// Bot votes 'Maybe' in the poll
|
||||||
|
botVoteForOption(bot, roomId, pollId, pollParams.options[2]);
|
||||||
|
|
||||||
|
// no votes shown until I vote, check bots vote has arrived
|
||||||
|
cy.get('.mx_MPollBody_totalVotes').should('contain', '1 vote cast');
|
||||||
|
|
||||||
|
// vote 'Maybe'
|
||||||
|
getPollOption(pollId, pollParams.options[2]).click('topLeft');
|
||||||
|
// both me and bot have voted Maybe
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||||
|
|
||||||
|
// change my vote to 'Yes'
|
||||||
|
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||||
|
|
||||||
|
// 1 vote for yes
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||||
|
// 1 vote for maybe
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 1);
|
||||||
|
|
||||||
|
// Bot updates vote to 'No'
|
||||||
|
botVoteForOption(bot, roomId, pollId, pollParams.options[1]);
|
||||||
|
|
||||||
|
// 1 vote for yes
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||||
|
// 1 vote for no
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||||
|
// 0 for maybe
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("displays polls correctly in thread panel", () => {
|
||||||
|
let botBob: MatrixClient;
|
||||||
|
let botCharlie: MatrixClient;
|
||||||
|
cy.getBot(synapse, { displayName: "BotBob" }).then(_bot => {
|
||||||
|
botBob = _bot;
|
||||||
|
});
|
||||||
|
cy.getBot(synapse, { displayName: "BotCharlie" }).then(_bot => {
|
||||||
|
botCharlie = _bot;
|
||||||
|
});
|
||||||
|
|
||||||
|
let roomId: string;
|
||||||
|
cy.createRoom({}).then(_roomId => {
|
||||||
|
roomId = _roomId;
|
||||||
|
cy.inviteUser(roomId, botBob.getUserId());
|
||||||
|
cy.inviteUser(roomId, botCharlie.getUserId());
|
||||||
|
cy.visit('/#/room/' + roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.openMessageComposerOptions().within(() => {
|
||||||
|
cy.get('[aria-label="Poll"]').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
const pollParams = {
|
||||||
|
title: 'Does the polls feature work?',
|
||||||
|
options: ['Yes', 'No', 'Maybe'],
|
||||||
|
};
|
||||||
|
createPoll(pollParams);
|
||||||
|
|
||||||
|
// Wait for message to send, get its ID and save as @pollId
|
||||||
|
cy.get(".mx_RoomView_body .mx_EventTile").contains(".mx_EventTile[data-scroll-tokens]", pollParams.title)
|
||||||
|
.invoke("attr", "data-scroll-tokens").as("pollId");
|
||||||
|
|
||||||
|
cy.get<string>("@pollId").then(pollId => {
|
||||||
|
// Bob starts thread on the poll
|
||||||
|
botBob.sendMessage(roomId, pollId, {
|
||||||
|
body: "Hello there",
|
||||||
|
msgtype: "m.text",
|
||||||
|
});
|
||||||
|
|
||||||
|
// open the thread summary
|
||||||
|
cy.get(".mx_RoomView_body .mx_ThreadSummary").click();
|
||||||
|
|
||||||
|
// Bob votes 'Maybe' in the poll
|
||||||
|
botVoteForOption(botBob, roomId, pollId, pollParams.options[2]);
|
||||||
|
// Charlie votes 'No'
|
||||||
|
botVoteForOption(botCharlie, roomId, pollId, pollParams.options[1]);
|
||||||
|
|
||||||
|
// no votes shown until I vote, check votes have arrived in main tl
|
||||||
|
cy.get('.mx_RoomView_body .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||||
|
// and thread view
|
||||||
|
cy.get('.mx_ThreadView .mx_MPollBody_totalVotes').should('contain', '2 votes cast');
|
||||||
|
|
||||||
|
cy.get('.mx_RoomView_body').within(() => {
|
||||||
|
// vote 'Maybe' in the main timeline poll
|
||||||
|
getPollOption(pollId, pollParams.options[2]).click('topLeft');
|
||||||
|
// both me and bob have voted Maybe
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get('.mx_ThreadView').within(() => {
|
||||||
|
// votes updated in thread view too
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 2);
|
||||||
|
// change my vote to 'Yes'
|
||||||
|
getPollOption(pollId, pollParams.options[0]).click('topLeft');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bob updates vote to 'No'
|
||||||
|
botVoteForOption(botBob, roomId, pollId, pollParams.options[1]);
|
||||||
|
|
||||||
|
// me: yes, bob: no, charlie: no
|
||||||
|
const expectVoteCounts = () => {
|
||||||
|
// I voted yes
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[0], 1);
|
||||||
|
// Bob and Charlie voted no
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[1], 2);
|
||||||
|
// 0 for maybe
|
||||||
|
expectPollOptionVoteCount(pollId, pollParams.options[2], 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check counts are correct in main timeline tile
|
||||||
|
cy.get('.mx_RoomView_body').within(() => {
|
||||||
|
expectVoteCounts();
|
||||||
|
});
|
||||||
|
// and in thread view tile
|
||||||
|
cy.get('.mx_ThreadView').within(() => {
|
||||||
|
expectVoteCounts();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,174 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import { SynapseInstance } from "../../plugins/synapsedocker";
|
||||||
|
import { MatrixClient } from "../../global";
|
||||||
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
interface Charly {
|
||||||
|
client: MatrixClient;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("Lazy Loading", () => {
|
||||||
|
let synapse: SynapseInstance;
|
||||||
|
let bob: MatrixClient;
|
||||||
|
const charlies: Charly[] = [];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.window().then(win => {
|
||||||
|
win.localStorage.setItem("mx_lhs_size", "0"); // Collapse left panel for these tests
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.startSynapse("default").then(data => {
|
||||||
|
synapse = data;
|
||||||
|
|
||||||
|
cy.initTestUser(synapse, "Alice");
|
||||||
|
|
||||||
|
cy.getBot(synapse, {
|
||||||
|
displayName: "Bob",
|
||||||
|
startClient: false,
|
||||||
|
autoAcceptInvites: false,
|
||||||
|
}).then(_bob => {
|
||||||
|
bob = _bob;
|
||||||
|
});
|
||||||
|
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
const displayName = `Charly #${i}`;
|
||||||
|
cy.getBot(synapse, {
|
||||||
|
displayName,
|
||||||
|
startClient: false,
|
||||||
|
autoAcceptInvites: false,
|
||||||
|
}).then(client => {
|
||||||
|
charlies[i - 1] = { displayName, client };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
cy.stopSynapse(synapse);
|
||||||
|
});
|
||||||
|
|
||||||
|
const name = "Lazy Loading Test";
|
||||||
|
const alias = "#lltest:localhost";
|
||||||
|
const charlyMsg1 = "hi bob!";
|
||||||
|
const charlyMsg2 = "how's it going??";
|
||||||
|
|
||||||
|
function setupRoomWithBobAliceAndCharlies(charlies: Charly[]) {
|
||||||
|
cy.window({ log: false }).then(win => {
|
||||||
|
return cy.wrap(bob.createRoom({
|
||||||
|
name,
|
||||||
|
room_alias_name: "lltest",
|
||||||
|
visibility: win.matrixcs.Visibility.Public,
|
||||||
|
}).then(r => r.room_id), { log: false }).as("roomId");
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.get<string>("@roomId").then(async roomId => {
|
||||||
|
for (const charly of charlies) {
|
||||||
|
await charly.client.joinRoom(alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const charly of charlies) {
|
||||||
|
cy.botSendMessage(charly.client, roomId, charlyMsg1);
|
||||||
|
}
|
||||||
|
for (const charly of charlies) {
|
||||||
|
cy.botSendMessage(charly.client, roomId, charlyMsg2);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 20; i >= 1; --i) {
|
||||||
|
cy.botSendMessage(bob, roomId, `I will only say this ${i} time(s)!`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.joinRoom(alias);
|
||||||
|
cy.viewRoomByName(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkPaginatedDisplayNames(charlies: Charly[]) {
|
||||||
|
cy.scrollToTop();
|
||||||
|
for (const charly of charlies) {
|
||||||
|
cy.findEventTile(charly.displayName, charlyMsg1).should("exist");
|
||||||
|
cy.findEventTile(charly.displayName, charlyMsg2).should("exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openMemberlist(): void {
|
||||||
|
cy.get('.mx_HeaderButtons [aria-label="Room Info"]').click();
|
||||||
|
cy.get(".mx_RoomSummaryCard").within(() => {
|
||||||
|
cy.get(".mx_RoomSummaryCard_icon_people").click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMembersInMemberlist(): Chainable<JQuery> {
|
||||||
|
return cy.get(".mx_MemberList .mx_EntityTile_name");
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMemberList(charlies: Charly[]) {
|
||||||
|
getMembersInMemberlist().contains("Alice").should("exist");
|
||||||
|
getMembersInMemberlist().contains("Bob").should("exist");
|
||||||
|
charlies.forEach(charly => {
|
||||||
|
getMembersInMemberlist().contains(charly.displayName).should("exist");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkMemberListLacksCharlies(charlies: Charly[]) {
|
||||||
|
charlies.forEach(charly => {
|
||||||
|
getMembersInMemberlist().contains(charly.displayName).should("not.exist");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function joinCharliesWhileAliceIsOffline(charlies: Charly[]) {
|
||||||
|
cy.goOffline();
|
||||||
|
|
||||||
|
cy.get<string>("@roomId").then(async roomId => {
|
||||||
|
for (const charly of charlies) {
|
||||||
|
await charly.client.joinRoom(alias);
|
||||||
|
}
|
||||||
|
for (let i = 20; i >= 1; --i) {
|
||||||
|
cy.botSendMessage(charlies[0].client, roomId, "where is charly?");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cy.goOnline();
|
||||||
|
cy.wait(1000); // Ideally we'd await a /sync here but intercepts step on each other from going offline/online
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should handle lazy loading properly even when offline", () => {
|
||||||
|
const charly1to5 = charlies.slice(0, 5);
|
||||||
|
const charly6to10 = charlies.slice(5);
|
||||||
|
|
||||||
|
// Set up room with alice, bob & charlies 1-5
|
||||||
|
setupRoomWithBobAliceAndCharlies(charly1to5);
|
||||||
|
// Alice should see 2 messages from every charly with the correct display name
|
||||||
|
checkPaginatedDisplayNames(charly1to5);
|
||||||
|
|
||||||
|
openMemberlist();
|
||||||
|
checkMemberList(charly1to5);
|
||||||
|
joinCharliesWhileAliceIsOffline(charly6to10);
|
||||||
|
checkMemberList(charly6to10);
|
||||||
|
|
||||||
|
cy.get<string>("@roomId").then(async roomId => {
|
||||||
|
for (const charly of charlies) {
|
||||||
|
await charly.client.leave(roomId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
checkMemberListLacksCharlies(charlies);
|
||||||
|
});
|
||||||
|
});
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
|
|
||||||
import request from "browser-request";
|
import request from "browser-request";
|
||||||
|
|
||||||
import type { 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 { SynapseInstance } from "../plugins/synapsedocker";
|
||||||
import Chainable = Cypress.Chainable;
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
@ -31,10 +31,15 @@ interface CreateBotOpts {
|
||||||
* The display name to give to that bot user
|
* The display name to give to that bot user
|
||||||
*/
|
*/
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
|
/**
|
||||||
|
* Whether or not to start the syncing client.
|
||||||
|
*/
|
||||||
|
startClient?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultCreateBotOptions = {
|
const defaultCreateBotOptions = {
|
||||||
autoAcceptInvites: true,
|
autoAcceptInvites: true,
|
||||||
|
startClient: true,
|
||||||
} as CreateBotOpts;
|
} as CreateBotOpts;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
@ -59,6 +64,13 @@ declare global {
|
||||||
* @param roomName Name of the room to join
|
* @param roomName Name of the room to join
|
||||||
*/
|
*/
|
||||||
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable<Room>;
|
botJoinRoomByName(cli: MatrixClient, roomName: string): Chainable<Room>;
|
||||||
|
/**
|
||||||
|
* Send a message as a bot into a room
|
||||||
|
* @param cli The bot's MatrixClient
|
||||||
|
* @param roomId ID of the room to join
|
||||||
|
* @param message the message body to send
|
||||||
|
*/
|
||||||
|
botSendMessage(cli: MatrixClient, roomId: string, message: string): Chainable<ISendEventResponse>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +100,10 @@ Cypress.Commands.add("getBot", (synapse: SynapseInstance, opts: CreateBotOpts):
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!opts.startClient) {
|
||||||
|
return cy.wrap(cli);
|
||||||
|
}
|
||||||
|
|
||||||
return cy.wrap(
|
return cy.wrap(
|
||||||
cli.initCrypto()
|
cli.initCrypto()
|
||||||
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
.then(() => cli.setGlobalErrorOnUnknownDevices(false))
|
||||||
|
@ -114,3 +130,14 @@ Cypress.Commands.add("botJoinRoomByName", (cli: MatrixClient, roomName: string):
|
||||||
|
|
||||||
return cy.wrap(Promise.reject());
|
return cy.wrap(Promise.reject());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("botSendMessage", (
|
||||||
|
cli: MatrixClient,
|
||||||
|
roomId: string,
|
||||||
|
message: string,
|
||||||
|
): Chainable<ISendEventResponse> => {
|
||||||
|
return cy.wrap(cli.sendMessage(roomId, {
|
||||||
|
msgtype: "m.text",
|
||||||
|
body: message,
|
||||||
|
}), { log: false });
|
||||||
|
});
|
||||||
|
|
|
@ -124,6 +124,11 @@ declare global {
|
||||||
* Boostraps cross-signing.
|
* Boostraps cross-signing.
|
||||||
*/
|
*/
|
||||||
bootstrapCrossSigning(): Chainable<void>;
|
bootstrapCrossSigning(): Chainable<void>;
|
||||||
|
/**
|
||||||
|
* Joins the given room by alias or ID
|
||||||
|
* @param roomIdOrAlias the id or alias of the room to join
|
||||||
|
*/
|
||||||
|
joinRoom(roomIdOrAlias: string): Chainable<Room>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -217,3 +222,7 @@ Cypress.Commands.add("bootstrapCrossSigning", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("joinRoom", (roomIdOrAlias: string): Chainable<Room> => {
|
||||||
|
return cy.getClient().then(cli => cli.joinRoom(roomIdOrAlias));
|
||||||
|
});
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
// Get the composer element
|
||||||
|
// selects main timeline composer by default
|
||||||
|
// set `isRightPanel` true to select right panel composer
|
||||||
|
getComposer(isRightPanel?: boolean): Chainable<JQuery>;
|
||||||
|
// Open the message composer kebab menu
|
||||||
|
openMessageComposerOptions(isRightPanel?: boolean): Chainable<JQuery>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("getComposer", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||||
|
const panelClass = isRightPanel ? '.mx_RightPanel' : '.mx_RoomView_body';
|
||||||
|
return cy.get(`${panelClass} .mx_MessageComposer`);
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("openMessageComposerOptions", (isRightPanel?: boolean): Chainable<JQuery> => {
|
||||||
|
cy.getComposer(isRightPanel).within(() => {
|
||||||
|
cy.get('[aria-label="More options"]').click();
|
||||||
|
});
|
||||||
|
return cy.get('.mx_MessageComposer_Menu');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needed to make this file a module
|
||||||
|
export { };
|
|
@ -33,3 +33,6 @@ import "./percy";
|
||||||
import "./webserver";
|
import "./webserver";
|
||||||
import "./views";
|
import "./views";
|
||||||
import "./iframes";
|
import "./iframes";
|
||||||
|
import "./timeline";
|
||||||
|
import "./network";
|
||||||
|
import "./composer";
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
// Intercept all /_matrix/ networking requests for the logged in user and fail them
|
||||||
|
goOffline(): void;
|
||||||
|
// Remove intercept on all /_matrix/ networking requests
|
||||||
|
goOnline(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We manage intercepting Matrix APIs here, as fully disabling networking will disconnect
|
||||||
|
// the browser under test from the Cypress runner, so can cause issues.
|
||||||
|
|
||||||
|
Cypress.Commands.add("goOffline", (): void => {
|
||||||
|
cy.log("Going offline");
|
||||||
|
cy.window({ log: false }).then(win => {
|
||||||
|
cy.intercept("**/_matrix/**", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||||
|
},
|
||||||
|
}, req => {
|
||||||
|
req.destroy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("goOnline", (): void => {
|
||||||
|
cy.log("Going online");
|
||||||
|
cy.window({ log: false }).then(win => {
|
||||||
|
cy.intercept("**/_matrix/**", {
|
||||||
|
headers: {
|
||||||
|
"Authorization": "Bearer " + win.mxMatrixClientPeg.matrixClient.getAccessToken(),
|
||||||
|
},
|
||||||
|
}, req => {
|
||||||
|
req.continue();
|
||||||
|
});
|
||||||
|
win.dispatchEvent(new Event("online"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needed to make this file a module
|
||||||
|
export { };
|
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/// <reference types="cypress" />
|
||||||
|
|
||||||
|
import Chainable = Cypress.Chainable;
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||||
|
namespace Cypress {
|
||||||
|
interface Chainable {
|
||||||
|
// Scroll to the top of the timeline
|
||||||
|
scrollToTop(): void;
|
||||||
|
// Find the event tile matching the given sender & body
|
||||||
|
findEventTile(sender: string, body: string): Chainable<JQuery>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Message {
|
||||||
|
sender: string;
|
||||||
|
body: string;
|
||||||
|
encrypted: boolean;
|
||||||
|
continuation: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cypress.Commands.add("scrollToTop", (): void => {
|
||||||
|
cy.get(".mx_RoomView_timeline .mx_ScrollPanel").scrollTo("top", { duration: 100 }).then(ref => {
|
||||||
|
if (ref.scrollTop() > 0) {
|
||||||
|
return cy.scrollToTop();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Cypress.Commands.add("findEventTile", (sender: string, body: string): Chainable<JQuery> => {
|
||||||
|
// We can't just use a bunch of `.contains` here due to continuations meaning that the events don't
|
||||||
|
// have their own rendered sender displayname so we have to walk the list to keep track of the sender.
|
||||||
|
return cy.get(".mx_RoomView_MessageList .mx_EventTile").then(refs => {
|
||||||
|
let latestSender: string;
|
||||||
|
for (let i = 0; i < refs.length; i++) {
|
||||||
|
const ref = refs.eq(i);
|
||||||
|
const displayName = ref.find(".mx_DisambiguatedProfile_displayName");
|
||||||
|
if (displayName) {
|
||||||
|
latestSender = displayName.text();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latestSender === sender && ref.find(".mx_EventTile_body").text() === body) {
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needed to make this file a module
|
||||||
|
export { };
|
|
@ -172,7 +172,7 @@
|
||||||
"blob-polyfill": "^6.0.20211015",
|
"blob-polyfill": "^6.0.20211015",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"cypress": "^10.3.0",
|
"cypress": "^10.3.0",
|
||||||
"cypress-real-events": "^1.7.0",
|
"cypress-real-events": "^1.7.1",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-to-json": "^3.6.2",
|
"enzyme-to-json": "^3.6.2",
|
||||||
"eslint": "8.9.0",
|
"eslint": "8.9.0",
|
||||||
|
|
|
@ -22,6 +22,8 @@ limitations under the License.
|
||||||
padding: $spacing-12 0;
|
padding: $spacing-12 0;
|
||||||
|
|
||||||
border-bottom: 1px solid $system;
|
border-bottom: 1px solid $system;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BeaconListItem_avatarIcon {
|
.mx_BeaconListItem_avatarIcon {
|
||||||
|
@ -61,3 +63,8 @@ limitations under the License.
|
||||||
color: $tertiary-content;
|
color: $tertiary-content;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_BeaconListItem_interactions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
|
@ -138,7 +138,7 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomStatusBar_unsentResendAllBtn {
|
&.mx_RoomStatusBar_unsentRetry {
|
||||||
padding-left: 34px; // 28px from above, but +6px to account for the wider icon
|
padding-left: 34px; // 28px from above, but +6px to account for the wider icon
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
|
|
|
@ -100,7 +100,7 @@ limitations under the License.
|
||||||
display: flex;
|
display: flex;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
|
||||||
&:hover {
|
&:not(.mx_RoomHeader_name--textonly):hover {
|
||||||
background-color: $quinary-content;
|
background-color: $quinary-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +139,7 @@ limitations under the License.
|
||||||
opacity: 0.6;
|
opacity: 0.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomHeader_name,
|
.mx_RoomHeader_name:not(.mx_RoomHeader_name--textonly),
|
||||||
.mx_RoomHeader_avatar {
|
.mx_RoomHeader_avatar {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { split } from "lodash";
|
||||||
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
import { isLocalRoom } from "./utils/localRoom/isLocalRoom";
|
||||||
|
|
||||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||||
export function avatarUrlForMember(
|
export function avatarUrlForMember(
|
||||||
|
@ -142,7 +143,12 @@ export function avatarUrlForRoom(room: Room, width: number, height: number, resi
|
||||||
if (room.isSpaceRoom()) return null;
|
if (room.isSpaceRoom()) return null;
|
||||||
|
|
||||||
// If the room is not a DM don't fallback to a member avatar
|
// If the room is not a DM don't fallback to a member avatar
|
||||||
if (!DMRoomMap.shared().getUserIdForRoomId(room.roomId)) return null;
|
if (
|
||||||
|
!DMRoomMap.shared().getUserIdForRoomId(room.roomId)
|
||||||
|
&& !(isLocalRoom(room))
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// If there are only two members in the DM use the avatar of the other member
|
// If there are only two members in the DM use the avatar of the other member
|
||||||
const otherMember = room.getAvatarFallbackMember();
|
const otherMember = room.getAvatarFallbackMember();
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
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 { MatrixClientPeg } from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
|
@ -44,6 +45,8 @@ export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
||||||
case EventType.RoomAliases:
|
case EventType.RoomAliases:
|
||||||
case EventType.RoomCanonicalAlias:
|
case EventType.RoomCanonicalAlias:
|
||||||
case EventType.RoomServerAcl:
|
case EventType.RoomServerAcl:
|
||||||
|
case M_BEACON.name:
|
||||||
|
case M_BEACON.altName:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,8 +29,9 @@ import QueryMatcher from './QueryMatcher';
|
||||||
import { PillCompletion } from './Components';
|
import { PillCompletion } from './Components';
|
||||||
import { ICompletion, ISelectionRange } from './Autocompleter';
|
import { ICompletion, ISelectionRange } from './Autocompleter';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import { EMOJI, IEmoji } from '../emoji';
|
import { EMOJI, IEmoji, getEmojiFromUnicode } from '../emoji';
|
||||||
import { TimelineRenderingType } from '../contexts/RoomContext';
|
import { TimelineRenderingType } from '../contexts/RoomContext';
|
||||||
|
import * as recent from '../emojipicker/recent';
|
||||||
|
|
||||||
const LIMIT = 20;
|
const LIMIT = 20;
|
||||||
|
|
||||||
|
@ -73,6 +74,7 @@ function colonsTrimmed(str: string): string {
|
||||||
export default class EmojiProvider extends AutocompleteProvider {
|
export default class EmojiProvider extends AutocompleteProvider {
|
||||||
matcher: QueryMatcher<ISortedEmoji>;
|
matcher: QueryMatcher<ISortedEmoji>;
|
||||||
nameMatcher: QueryMatcher<ISortedEmoji>;
|
nameMatcher: QueryMatcher<ISortedEmoji>;
|
||||||
|
private readonly recentlyUsed: IEmoji[];
|
||||||
|
|
||||||
constructor(room: Room, renderingType?: TimelineRenderingType) {
|
constructor(room: Room, renderingType?: TimelineRenderingType) {
|
||||||
super({ commandRegex: EMOJI_REGEX, renderingType });
|
super({ commandRegex: EMOJI_REGEX, renderingType });
|
||||||
|
@ -87,6 +89,8 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
// For removing punctuation
|
// For removing punctuation
|
||||||
shouldMatchWordsOnly: true,
|
shouldMatchWordsOnly: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.recentlyUsed = Array.from(new Set(recent.get().map(getEmojiFromUnicode).filter(Boolean)));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCompletions(
|
async getCompletions(
|
||||||
|
@ -109,7 +113,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
// Do second match with shouldMatchWordsOnly in order to match against 'name'
|
||||||
completions = completions.concat(this.nameMatcher.match(matchedString));
|
completions = completions.concat(this.nameMatcher.match(matchedString));
|
||||||
|
|
||||||
const sorters = [];
|
let sorters = [];
|
||||||
// make sure that emoticons come first
|
// make sure that emoticons come first
|
||||||
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
|
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
|
||||||
|
|
||||||
|
@ -130,6 +134,15 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
sorters.push(c => c._orderBy);
|
sorters.push(c => c._orderBy);
|
||||||
completions = sortBy(uniq(completions), sorters);
|
completions = sortBy(uniq(completions), sorters);
|
||||||
|
|
||||||
|
completions = completions.slice(0, LIMIT);
|
||||||
|
|
||||||
|
// Do a second sort to place emoji matching with frequently used one on top
|
||||||
|
sorters = [];
|
||||||
|
this.recentlyUsed.forEach(emoji => {
|
||||||
|
sorters.push(c => score(emoji.shortcodes[0], c.emoji.shortcodes[0]));
|
||||||
|
});
|
||||||
|
completions = sortBy(uniq(completions), sorters);
|
||||||
|
|
||||||
completions = completions.map(c => ({
|
completions = completions.map(c => ({
|
||||||
completion: c.emoji.unicode,
|
completion: c.emoji.unicode,
|
||||||
component: (
|
component: (
|
||||||
|
@ -138,7 +151,7 @@ export default class EmojiProvider extends AutocompleteProvider {
|
||||||
</PillCompletion>
|
</PillCompletion>
|
||||||
),
|
),
|
||||||
range,
|
range,
|
||||||
})).slice(0, LIMIT);
|
}));
|
||||||
}
|
}
|
||||||
return completions;
|
return completions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,6 +132,7 @@ import VideoChannelStore from "../../stores/VideoChannelStore";
|
||||||
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
import { IRoomStateEventsActionPayload } from "../../actions/MatrixActionCreators";
|
||||||
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
|
import { UseCaseSelection } from '../views/elements/UseCaseSelection';
|
||||||
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
import { ValidatedServerConfig } from '../../utils/ValidatedServerConfig';
|
||||||
|
import { isLocalRoom } from '../../utils/localRoom/isLocalRoom';
|
||||||
|
|
||||||
// legacy export
|
// legacy export
|
||||||
export { default as Views } from "../../Views";
|
export { default as Views } from "../../Views";
|
||||||
|
@ -890,7 +891,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
// If we are redirecting to a Room Alias and it is for the room we already showing then replace history item
|
||||||
const replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
let replaceLast = presentedId[0] === "#" && roomInfo.room_id === this.state.currentRoomId;
|
||||||
|
|
||||||
|
if (isLocalRoom(this.state.currentRoomId)) {
|
||||||
|
// Replace local room history items
|
||||||
|
replaceLast = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (roomInfo.room_id === this.state.currentRoomId) {
|
if (roomInfo.room_id === this.state.currentRoomId) {
|
||||||
// if we are re-viewing the same room then copy any state we already know
|
// if we are re-viewing the same room then copy any state we already know
|
||||||
|
|
|
@ -24,11 +24,11 @@ import Resend from '../../Resend';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
import { messageForResourceLimitError } from '../../utils/ErrorUtils';
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import NotificationBadge from "../views/rooms/NotificationBadge";
|
|
||||||
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
import { RoomStatusBarUnsentMessages } from './RoomStatusBarUnsentMessages';
|
||||||
|
|
||||||
const STATUS_BAR_HIDDEN = 0;
|
const STATUS_BAR_HIDDEN = 0;
|
||||||
const STATUS_BAR_EXPANDED = 1;
|
const STATUS_BAR_EXPANDED = 1;
|
||||||
|
@ -240,7 +240,7 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||||
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
<AccessibleButton onClick={this.onCancelAllClick} className="mx_RoomStatusBar_unsentCancelAllBtn">
|
||||||
{ _t("Delete all") }
|
{ _t("Delete all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentResendAllBtn">
|
<AccessibleButton onClick={this.onResendAllClick} className="mx_RoomStatusBar_unsentRetry">
|
||||||
{ _t("Retry all") }
|
{ _t("Retry all") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</>;
|
</>;
|
||||||
|
@ -252,28 +252,12 @@ export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>
|
return <RoomStatusBarUnsentMessages
|
||||||
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
|
title={title}
|
||||||
<div role="alert">
|
description={_t("You can select all or individual messages to retry or delete")}
|
||||||
<div className="mx_RoomStatusBar_unsentBadge">
|
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||||
<NotificationBadge
|
buttons={buttonRow}
|
||||||
notification={StaticNotificationState.RED_EXCLAMATION}
|
/>;
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mx_RoomStatusBar_unsentTitle">
|
|
||||||
{ title }
|
|
||||||
</div>
|
|
||||||
<div className="mx_RoomStatusBar_unsentDescription">
|
|
||||||
{ _t("You can select all or individual messages to retry or delete") }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx_RoomStatusBar_unsentButtonBar">
|
|
||||||
{ buttonRow }
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public render(): JSX.Element {
|
public render(): JSX.Element {
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactElement } from "react";
|
||||||
|
|
||||||
|
import { StaticNotificationState } from "../../stores/notifications/StaticNotificationState";
|
||||||
|
import NotificationBadge from "../views/rooms/NotificationBadge";
|
||||||
|
|
||||||
|
interface RoomStatusBarUnsentMessagesProps {
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
notificationState: StaticNotificationState;
|
||||||
|
buttons: ReactElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoomStatusBarUnsentMessages = (props: RoomStatusBarUnsentMessagesProps): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="mx_RoomStatusBar mx_RoomStatusBar_unsentMessages">
|
||||||
|
<div role="alert">
|
||||||
|
<div className="mx_RoomStatusBar_unsentBadge">
|
||||||
|
<NotificationBadge
|
||||||
|
notification={props.notificationState}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="mx_RoomStatusBar_unsentTitle">
|
||||||
|
{ props.title }
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
props.description &&
|
||||||
|
<div className="mx_RoomStatusBar_unsentDescription">
|
||||||
|
{ props.description }
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className="mx_RoomStatusBar_unsentButtonBar">
|
||||||
|
{ props.buttons }
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useContext } from 'react';
|
import React, { HTMLProps, useContext } from 'react';
|
||||||
import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
import { Beacon, BeaconEvent } from 'matrix-js-sdk/src/matrix';
|
||||||
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
import { LocationAssetType } from 'matrix-js-sdk/src/@types/location';
|
||||||
|
|
||||||
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../contexts/MatrixClientContext';
|
||||||
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
import { useEventEmitterState } from '../../../hooks/useEventEmitter';
|
||||||
import { humanizeTime } from '../../../utils/humanize';
|
import { humanizeTime } from '../../../utils/humanize';
|
||||||
|
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import MemberAvatar from '../avatars/MemberAvatar';
|
import MemberAvatar from '../avatars/MemberAvatar';
|
||||||
import BeaconStatus from './BeaconStatus';
|
import BeaconStatus from './BeaconStatus';
|
||||||
|
@ -32,7 +33,7 @@ interface Props {
|
||||||
beacon: Beacon;
|
beacon: Beacon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
const BeaconListItem: React.FC<Props & HTMLProps<HTMLLIElement>> = ({ beacon, ...rest }) => {
|
||||||
const latestLocationState = useEventEmitterState(
|
const latestLocationState = useEventEmitterState(
|
||||||
beacon,
|
beacon,
|
||||||
BeaconEvent.LocationUpdate,
|
BeaconEvent.LocationUpdate,
|
||||||
|
@ -52,7 +53,7 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
||||||
|
|
||||||
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
|
const humanizedUpdateTime = humanizeTime(latestLocationState.timestamp);
|
||||||
|
|
||||||
return <li className='mx_BeaconListItem'>
|
return <li className='mx_BeaconListItem' {...rest}>
|
||||||
{ isSelfLocation ?
|
{ isSelfLocation ?
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
className='mx_BeaconListItem_avatar'
|
className='mx_BeaconListItem_avatar'
|
||||||
|
@ -69,7 +70,11 @@ const BeaconListItem: React.FC<Props> = ({ beacon }) => {
|
||||||
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
|
label={beaconMember?.name || beacon.beaconInfo.description || beacon.beaconInfoOwner}
|
||||||
displayStatus={BeaconDisplayStatus.Active}
|
displayStatus={BeaconDisplayStatus.Active}
|
||||||
>
|
>
|
||||||
|
{ /* eat events from interactive share buttons
|
||||||
|
so parent click handlers are not triggered */ }
|
||||||
|
<div className='mx_BeaconListItem_interactions' onClick={preventDefaultWrapper(() => {})}>
|
||||||
<ShareLatestLocation latestLocationState={latestLocationState} />
|
<ShareLatestLocation latestLocationState={latestLocationState} />
|
||||||
|
</div>
|
||||||
</BeaconStatus>
|
</BeaconStatus>
|
||||||
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
|
<span className='mx_BeaconListItem_lastUpdated'>{ _t("Updated %(humanizedUpdateTime)s", { humanizedUpdateTime }) }</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import {
|
import {
|
||||||
Beacon,
|
Beacon,
|
||||||
|
@ -45,7 +45,16 @@ interface IProps extends IDialogProps {
|
||||||
roomId: Room['roomId'];
|
roomId: Room['roomId'];
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
// open the map centered on this beacon's location
|
// open the map centered on this beacon's location
|
||||||
focusBeacon?: Beacon;
|
initialFocusedBeacon?: Beacon;
|
||||||
|
}
|
||||||
|
|
||||||
|
// track the 'focused time' as ts
|
||||||
|
// to make it possible to refocus the same beacon
|
||||||
|
// as the beacon location may change
|
||||||
|
// or the map may move around
|
||||||
|
interface FocusedBeaconState {
|
||||||
|
ts: number;
|
||||||
|
beacon?: Beacon;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||||
|
@ -59,31 +68,52 @@ const getBoundsCenter = (bounds: Bounds): string | undefined => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useInitialMapPosition = (liveBeacons: Beacon[], focusBeacon?: Beacon): {
|
const useInitialMapPosition = (liveBeacons: Beacon[], { beacon, ts }: FocusedBeaconState): {
|
||||||
bounds?: Bounds; centerGeoUri: string;
|
bounds?: Bounds; centerGeoUri: string;
|
||||||
} => {
|
} => {
|
||||||
const bounds = useRef<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
const [bounds, setBounds] = useState<Bounds | undefined>(getBeaconBounds(liveBeacons));
|
||||||
const centerGeoUri = useRef<string>(
|
const [centerGeoUri, setCenterGeoUri] = useState<string>(
|
||||||
focusBeacon?.latestLocationState?.uri ||
|
beacon?.latestLocationState?.uri ||
|
||||||
getBoundsCenter(bounds.current),
|
getBoundsCenter(bounds),
|
||||||
);
|
);
|
||||||
return { bounds: bounds.current, centerGeoUri: centerGeoUri.current };
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
// this check ignores the first initial focused beacon state
|
||||||
|
// as centering logic on map zooms to show everything
|
||||||
|
// instead of focusing down
|
||||||
|
ts !== 0 &&
|
||||||
|
// only set focus to a known location
|
||||||
|
beacon?.latestLocationState?.uri
|
||||||
|
) {
|
||||||
|
// append custom `mxTs` parameter to geoUri
|
||||||
|
// so map is triggered to refocus on this uri
|
||||||
|
// event if it was previously the center geouri
|
||||||
|
// but the map have moved/zoomed
|
||||||
|
setCenterGeoUri(`${beacon?.latestLocationState?.uri};mxTs=${Date.now()}`);
|
||||||
|
setBounds(getBeaconBounds([beacon]));
|
||||||
|
}
|
||||||
|
}, [beacon, ts]);
|
||||||
|
|
||||||
|
return { bounds, centerGeoUri };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog to view live beacons maximised
|
* Dialog to view live beacons maximised
|
||||||
*/
|
*/
|
||||||
const BeaconViewDialog: React.FC<IProps> = ({
|
const BeaconViewDialog: React.FC<IProps> = ({
|
||||||
focusBeacon,
|
initialFocusedBeacon,
|
||||||
roomId,
|
roomId,
|
||||||
matrixClient,
|
matrixClient,
|
||||||
onFinished,
|
onFinished,
|
||||||
}) => {
|
}) => {
|
||||||
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
const liveBeacons = useLiveBeacons(roomId, matrixClient);
|
||||||
|
const [focusedBeaconState, setFocusedBeaconState] =
|
||||||
|
useState<FocusedBeaconState>({ beacon: initialFocusedBeacon, ts: 0 });
|
||||||
|
|
||||||
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
const [isSidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusBeacon);
|
const { bounds, centerGeoUri } = useInitialMapPosition(liveBeacons, focusedBeaconState);
|
||||||
|
|
||||||
const [mapDisplayError, setMapDisplayError] = useState<Error>();
|
const [mapDisplayError, setMapDisplayError] = useState<Error>();
|
||||||
|
|
||||||
|
@ -94,6 +124,10 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
||||||
}
|
}
|
||||||
}, [mapDisplayError]);
|
}, [mapDisplayError]);
|
||||||
|
|
||||||
|
const onBeaconListItemClick = (beacon: Beacon) => {
|
||||||
|
setFocusedBeaconState({ beacon, ts: Date.now() });
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_BeaconViewDialog'
|
className='mx_BeaconViewDialog'
|
||||||
|
@ -144,7 +178,7 @@ const BeaconViewDialog: React.FC<IProps> = ({
|
||||||
</MapFallback>
|
</MapFallback>
|
||||||
}
|
}
|
||||||
{ isSidebarOpen ?
|
{ isSidebarOpen ?
|
||||||
<DialogSidebar beacons={liveBeacons} requestClose={() => setSidebarOpen(false)} /> :
|
<DialogSidebar beacons={liveBeacons} onBeaconClick={onBeaconListItemClick} requestClose={() => setSidebarOpen(false)} /> :
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind='primary'
|
kind='primary'
|
||||||
onClick={() => setSidebarOpen(true)}
|
onClick={() => setSidebarOpen(true)}
|
||||||
|
|
|
@ -26,9 +26,14 @@ import BeaconListItem from './BeaconListItem';
|
||||||
interface Props {
|
interface Props {
|
||||||
beacons: Beacon[];
|
beacons: Beacon[];
|
||||||
requestClose: () => void;
|
requestClose: () => void;
|
||||||
|
onBeaconClick: (beacon: Beacon) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
const DialogSidebar: React.FC<Props> = ({
|
||||||
|
beacons,
|
||||||
|
onBeaconClick,
|
||||||
|
requestClose,
|
||||||
|
}) => {
|
||||||
return <div className='mx_DialogSidebar'>
|
return <div className='mx_DialogSidebar'>
|
||||||
<div className='mx_DialogSidebar_header'>
|
<div className='mx_DialogSidebar_header'>
|
||||||
<Heading size='h4'>{ _t('View List') }</Heading>
|
<Heading size='h4'>{ _t('View List') }</Heading>
|
||||||
|
@ -36,13 +41,17 @@ const DialogSidebar: React.FC<Props> = ({ beacons, requestClose }) => {
|
||||||
className='mx_DialogSidebar_closeButton'
|
className='mx_DialogSidebar_closeButton'
|
||||||
onClick={requestClose}
|
onClick={requestClose}
|
||||||
title={_t('Close sidebar')}
|
title={_t('Close sidebar')}
|
||||||
data-test-id='dialog-sidebar-close'
|
data-testid='dialog-sidebar-close'
|
||||||
>
|
>
|
||||||
<CloseIcon className='mx_DialogSidebar_closeButtonIcon' />
|
<CloseIcon className='mx_DialogSidebar_closeButtonIcon' />
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<ol className='mx_DialogSidebar_list'>
|
<ol className='mx_DialogSidebar_list'>
|
||||||
{ beacons.map((beacon) => <BeaconListItem key={beacon.identifier} beacon={beacon} />) }
|
{ beacons.map((beacon) => <BeaconListItem
|
||||||
|
key={beacon.identifier}
|
||||||
|
beacon={beacon}
|
||||||
|
onClick={() => onBeaconClick(beacon)}
|
||||||
|
/>) }
|
||||||
</ol>
|
</ol>
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,6 +19,7 @@ import React, { HTMLProps } from 'react';
|
||||||
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { useOwnLiveBeacons } from '../../../utils/beacon';
|
import { useOwnLiveBeacons } from '../../../utils/beacon';
|
||||||
|
import { preventDefaultWrapper } from '../../../utils/NativeEventUtils';
|
||||||
import BeaconStatus from './BeaconStatus';
|
import BeaconStatus from './BeaconStatus';
|
||||||
import { BeaconDisplayStatus } from './displayStatus';
|
import { BeaconDisplayStatus } from './displayStatus';
|
||||||
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
import AccessibleButton, { ButtonEvent } from '../elements/AccessibleButton';
|
||||||
|
@ -45,14 +46,6 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
onResetLocationPublishError,
|
onResetLocationPublishError,
|
||||||
} = useOwnLiveBeacons([beacon?.identifier]);
|
} = useOwnLiveBeacons([beacon?.identifier]);
|
||||||
|
|
||||||
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
|
||||||
// moving under the beacon status on stop/retry click
|
|
||||||
const preventDefaultWrapper = (callback: () => void) => (e?: ButtonEvent) => {
|
|
||||||
e?.stopPropagation();
|
|
||||||
e?.preventDefault();
|
|
||||||
callback();
|
|
||||||
};
|
|
||||||
|
|
||||||
// combine display status with errors that only occur for user's own beacons
|
// combine display status with errors that only occur for user's own beacons
|
||||||
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
|
const ownDisplayStatus = hasLocationPublishError || hasStopSharingError ?
|
||||||
BeaconDisplayStatus.Error :
|
BeaconDisplayStatus.Error :
|
||||||
|
@ -68,7 +61,9 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
|
{ ownDisplayStatus === BeaconDisplayStatus.Active && <AccessibleButton
|
||||||
data-test-id='beacon-status-stop-beacon'
|
data-test-id='beacon-status-stop-beacon'
|
||||||
kind='link'
|
kind='link'
|
||||||
onClick={preventDefaultWrapper(onStopSharing)}
|
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||||
|
// moving under the beacon status on stop/retry click
|
||||||
|
onClick={preventDefaultWrapper<ButtonEvent>(onStopSharing)}
|
||||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||||
disabled={stoppingInProgress}
|
disabled={stoppingInProgress}
|
||||||
>
|
>
|
||||||
|
@ -78,6 +73,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
{ hasLocationPublishError && <AccessibleButton
|
{ hasLocationPublishError && <AccessibleButton
|
||||||
data-test-id='beacon-status-reset-wire-error'
|
data-test-id='beacon-status-reset-wire-error'
|
||||||
kind='link'
|
kind='link'
|
||||||
|
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||||
|
// moving under the beacon status on stop/retry click
|
||||||
onClick={preventDefaultWrapper(onResetLocationPublishError)}
|
onClick={preventDefaultWrapper(onResetLocationPublishError)}
|
||||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||||
>
|
>
|
||||||
|
@ -87,6 +84,8 @@ const OwnBeaconStatus: React.FC<Props & HTMLProps<HTMLDivElement>> = ({
|
||||||
{ hasStopSharingError && <AccessibleButton
|
{ hasStopSharingError && <AccessibleButton
|
||||||
data-test-id='beacon-status-stop-beacon-retry'
|
data-test-id='beacon-status-stop-beacon-retry'
|
||||||
kind='link'
|
kind='link'
|
||||||
|
// eat events here to avoid 1) the map and 2) reply or thread tiles
|
||||||
|
// moving under the beacon status on stop/retry click
|
||||||
onClick={preventDefaultWrapper(onStopSharing)}
|
onClick={preventDefaultWrapper(onStopSharing)}
|
||||||
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
className='mx_OwnBeaconStatus_button mx_OwnBeaconStatus_destructiveButton'
|
||||||
>
|
>
|
||||||
|
|
|
@ -91,6 +91,7 @@ import { PublicRoomResultDetails } from "./PublicRoomResultDetails";
|
||||||
import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
import { RoomResultContextMenus } from "./RoomResultContextMenus";
|
||||||
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
import { RoomContextDetails } from "../../rooms/RoomContextDetails";
|
||||||
import { TooltipOption } from "./TooltipOption";
|
import { TooltipOption } from "./TooltipOption";
|
||||||
|
import { isLocalRoom } from "../../../../utils/localRoom/isLocalRoom";
|
||||||
|
|
||||||
const MAX_RECENT_SEARCHES = 10;
|
const MAX_RECENT_SEARCHES = 10;
|
||||||
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
const SECTION_LIMIT = 50; // only show 50 results per section for performance reasons
|
||||||
|
@ -243,6 +244,9 @@ export const useWebSearchMetrics = (numResults: number, queryLength: number, via
|
||||||
|
|
||||||
const findVisibleRooms = (cli: MatrixClient) => {
|
const findVisibleRooms = (cli: MatrixClient) => {
|
||||||
return cli.getVisibleRooms().filter(room => {
|
return cli.getVisibleRooms().filter(room => {
|
||||||
|
// Do not show local rooms
|
||||||
|
if (isLocalRoom(room)) return false;
|
||||||
|
|
||||||
// TODO we may want to put invites in their own list
|
// TODO we may want to put invites in their own list
|
||||||
return room.getMyMembership() === "join" || room.getMyMembership() == "invite";
|
return room.getMyMembership() === "join" || room.getMyMembership() == "invite";
|
||||||
});
|
});
|
||||||
|
@ -395,7 +399,7 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
|
|
||||||
possibleResults.forEach(entry => {
|
possibleResults.forEach(entry => {
|
||||||
if (isRoomResult(entry)) {
|
if (isRoomResult(entry)) {
|
||||||
if (!entry.room.normalizedName.includes(normalizedQuery) &&
|
if (!entry.room.normalizedName?.includes(normalizedQuery) &&
|
||||||
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
!entry.room.getCanonicalAlias()?.toLowerCase().includes(lcQuery) &&
|
||||||
!entry.query?.some(q => q.includes(lcQuery))
|
!entry.query?.some(q => q.includes(lcQuery))
|
||||||
) return; // bail, does not match query
|
) return; // bail, does not match query
|
||||||
|
@ -603,6 +607,16 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
}
|
}
|
||||||
if (isPublicRoomResult(result)) {
|
if (isPublicRoomResult(result)) {
|
||||||
const clientRoom = cli.getRoom(result.publicRoom.room_id);
|
const clientRoom = cli.getRoom(result.publicRoom.room_id);
|
||||||
|
// Element Web currently does not allow guests to join rooms, so we
|
||||||
|
// instead show them view buttons for all rooms. If the room is not
|
||||||
|
// world readable, a modal will appear asking you to register first. If
|
||||||
|
// it is readable, the preview appears as normal.
|
||||||
|
const showViewButton = (
|
||||||
|
clientRoom?.getMyMembership() === "join" ||
|
||||||
|
result.publicRoom.world_readable ||
|
||||||
|
cli.isGuest()
|
||||||
|
);
|
||||||
|
|
||||||
const listener = (ev) => {
|
const listener = (ev) => {
|
||||||
const { publicRoom } = result;
|
const { publicRoom } = result;
|
||||||
viewRoom({
|
viewRoom({
|
||||||
|
@ -618,11 +632,11 @@ const SpotlightDialog: React.FC<IProps> = ({ initialText = "", initialFilter = n
|
||||||
onClick={listener}
|
onClick={listener}
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind={clientRoom ? "primary" : "primary_outline"}
|
kind={showViewButton ? "primary_outline" : "primary"}
|
||||||
onClick={listener}
|
onClick={listener}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
{ _t(clientRoom ? "View" : "Join") }
|
{ showViewButton ? _t("View") : _t("Join") }
|
||||||
</AccessibleButton>}
|
</AccessibleButton>}
|
||||||
aria-labelledby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_name`}
|
aria-labelledby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_name`}
|
||||||
aria-describedby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
|
aria-describedby={`mx_SpotlightDialog_button_result_${result.publicRoom.room_id}_alias`}
|
||||||
|
|
|
@ -80,6 +80,13 @@ const useMapWithStyle = ({ id, centerGeoUri, onError, interactive, bounds }) =>
|
||||||
interface MapProps {
|
interface MapProps {
|
||||||
id: string;
|
id: string;
|
||||||
interactive?: boolean;
|
interactive?: boolean;
|
||||||
|
/**
|
||||||
|
* set map center to geoUri coords
|
||||||
|
* Center will only be set to valid geoUri
|
||||||
|
* this prop is only simply diffed by useEffect, so to trigger *recentering* of the same geoUri
|
||||||
|
* append the uri with a var not used by the geoUri spec
|
||||||
|
* eg a timestamp: `geo:54,42;mxTs=123`
|
||||||
|
*/
|
||||||
centerGeoUri?: string;
|
centerGeoUri?: string;
|
||||||
bounds?: Bounds;
|
bounds?: Bounds;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
|
@ -24,6 +24,7 @@ import EventTileBubble from "./EventTileBubble";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { objectHasDiff } from "../../../utils/objects";
|
import { objectHasDiff } from "../../../utils/objects";
|
||||||
|
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -46,12 +47,15 @@ const EncryptionEvent = forwardRef<HTMLDivElement, IProps>(({ mxEvent, timestamp
|
||||||
if (content.algorithm === ALGORITHM && isRoomEncrypted) {
|
if (content.algorithm === ALGORITHM && isRoomEncrypted) {
|
||||||
let subtitle: string;
|
let subtitle: string;
|
||||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
|
const room = cli?.getRoom(roomId);
|
||||||
if (prevContent.algorithm === ALGORITHM) {
|
if (prevContent.algorithm === ALGORITHM) {
|
||||||
subtitle = _t("Some encryption parameters have been changed.");
|
subtitle = _t("Some encryption parameters have been changed.");
|
||||||
} else if (dmPartner) {
|
} else if (dmPartner) {
|
||||||
const displayName = cli?.getRoom(roomId)?.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
const displayName = room.getMember(dmPartner)?.rawDisplayName || dmPartner;
|
||||||
subtitle = _t("Messages here are end-to-end encrypted. " +
|
subtitle = _t("Messages here are end-to-end encrypted. " +
|
||||||
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
|
"Verify %(displayName)s in their profile - tap on their avatar.", { displayName });
|
||||||
|
} else if (isLocalRoom(room)) {
|
||||||
|
subtitle = _t("Messages in this chat will be end-to-end encrypted.");
|
||||||
} else {
|
} else {
|
||||||
subtitle = _t("Messages in this room are end-to-end encrypted. " +
|
subtitle = _t("Messages in this room are end-to-end encrypted. " +
|
||||||
"When people join, you can verify them in their profile, just tap on their avatar.");
|
"When people join, you can verify them in their profile, just tap on their avatar.");
|
||||||
|
|
|
@ -162,7 +162,7 @@ const MBeaconBody: React.FC<IBodyProps> = React.forwardRef(({ mxEvent, getRelati
|
||||||
{
|
{
|
||||||
roomId: mxEvent.getRoomId(),
|
roomId: mxEvent.getRoomId(),
|
||||||
matrixClient,
|
matrixClient,
|
||||||
focusBeacon: beacon,
|
initialFocusedBeacon: beacon,
|
||||||
isMapDisplayError,
|
isMapDisplayError,
|
||||||
},
|
},
|
||||||
"mx_BeaconViewDialog_wrapper",
|
"mx_BeaconViewDialog_wrapper",
|
||||||
|
|
|
@ -80,6 +80,7 @@ import { haveRendererForEvent, isMessageEvent, renderTile } from "../../../event
|
||||||
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
import ThreadSummary, { ThreadMessagePreview } from "./ThreadSummary";
|
||||||
import { ReadReceiptGroup } from './ReadReceiptGroup';
|
import { ReadReceiptGroup } from './ReadReceiptGroup';
|
||||||
import { useTooltip } from "../../../utils/useTooltip";
|
import { useTooltip } from "../../../utils/useTooltip";
|
||||||
|
import { isLocalRoom } from '../../../utils/localRoom/isLocalRoom';
|
||||||
|
|
||||||
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
export type GetRelationsForEvent = (eventId: string, relationType: string, eventType: string) => Relations;
|
||||||
|
|
||||||
|
@ -766,6 +767,9 @@ export class UnwrappedEventTile extends React.Component<IProps, IState> {
|
||||||
private renderE2EPadlock() {
|
private renderE2EPadlock() {
|
||||||
const ev = this.props.mxEvent;
|
const ev = this.props.mxEvent;
|
||||||
|
|
||||||
|
// no icon for local rooms
|
||||||
|
if (isLocalRoom(ev.getRoomId())) return;
|
||||||
|
|
||||||
// event could not be decrypted
|
// event could not be decrypted
|
||||||
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
if (ev.getContent().msgtype === 'm.bad.encrypted') {
|
||||||
return <E2ePadlockUndecryptable />;
|
return <E2ePadlockUndecryptable />;
|
||||||
|
|
|
@ -38,6 +38,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
|
||||||
import { UIComponent } from "../../../settings/UIFeature";
|
import { UIComponent } from "../../../settings/UIFeature";
|
||||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||||
|
import { LocalRoom } from "../../../models/LocalRoom";
|
||||||
|
|
||||||
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean {
|
||||||
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId);
|
||||||
|
@ -49,11 +50,19 @@ const NewRoomIntro = () => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
const { room, roomId } = useContext(RoomContext);
|
const { room, roomId } = useContext(RoomContext);
|
||||||
|
|
||||||
const dmPartner = DMRoomMap.shared().getUserIdForRoomId(roomId);
|
const isLocalRoom = room instanceof LocalRoom;
|
||||||
|
const dmPartner = isLocalRoom
|
||||||
|
? room.targets[0]?.userId
|
||||||
|
: DMRoomMap.shared().getUserIdForRoomId(roomId);
|
||||||
|
|
||||||
let body;
|
let body;
|
||||||
if (dmPartner) {
|
if (dmPartner) {
|
||||||
|
let introMessage = _t("This is the beginning of your direct message history with <displayName/>.");
|
||||||
let caption;
|
let caption;
|
||||||
if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
|
||||||
|
if (isLocalRoom) {
|
||||||
|
introMessage = _t("Send your first message to invite <displayName/> to chat");
|
||||||
|
} else if ((room.getJoinedMemberCount() + room.getInvitedMemberCount()) === 2) {
|
||||||
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
caption = _t("Only the two of you are in this conversation, unless either of you invites anyone to join.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,7 +84,7 @@ const NewRoomIntro = () => {
|
||||||
|
|
||||||
<h2>{ room.name }</h2>
|
<h2>{ room.name }</h2>
|
||||||
|
|
||||||
<p>{ _t("This is the beginning of your direct message history with <displayName/>.", {}, {
|
<p>{ _t(introMessage, {}, {
|
||||||
displayName: () => <b>{ displayName }</b>,
|
displayName: () => <b>{ displayName }</b>,
|
||||||
}) }</p>
|
}) }</p>
|
||||||
{ caption && <p>{ caption }</p> }
|
{ caption && <p>{ caption }</p> }
|
||||||
|
@ -200,7 +209,7 @@ const NewRoomIntro = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
let subButton;
|
let subButton;
|
||||||
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get())) {
|
if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.get()) && !isLocalRoom) {
|
||||||
subButton = (
|
subButton = (
|
||||||
<AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
|
<AccessibleButton kind='link_inline' onClick={openRoomSettings}>{ _t("Enable encryption in settings.") }</AccessibleButton>
|
||||||
);
|
);
|
||||||
|
|
|
@ -65,6 +65,8 @@ interface IProps {
|
||||||
appsShown: boolean;
|
appsShown: boolean;
|
||||||
searchInfo: ISearchInfo;
|
searchInfo: ISearchInfo;
|
||||||
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
excludedRightPanelPhaseButtons?: Array<RightPanelPhases>;
|
||||||
|
showButtons?: boolean;
|
||||||
|
enableRoomOptionsMenu?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
@ -76,6 +78,8 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
editing: false,
|
editing: false,
|
||||||
inRoom: false,
|
inRoom: false,
|
||||||
excludedRightPanelPhaseButtons: [],
|
excludedRightPanelPhaseButtons: [],
|
||||||
|
showButtons: true,
|
||||||
|
enableRoomOptionsMenu: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = RoomContext;
|
static contextType = RoomContext;
|
||||||
|
@ -130,81 +134,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
this.setState({ contextMenuPosition: null });
|
this.setState({ contextMenuPosition: null });
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
private renderButtons(): JSX.Element[] {
|
||||||
let searchStatus = null;
|
|
||||||
|
|
||||||
// don't display the search count until the search completes and
|
|
||||||
// gives us a valid (possibly zero) searchCount.
|
|
||||||
if (this.props.searchInfo &&
|
|
||||||
this.props.searchInfo.searchCount !== undefined &&
|
|
||||||
this.props.searchInfo.searchCount !== null) {
|
|
||||||
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
|
||||||
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
|
||||||
let settingsHint = false;
|
|
||||||
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
|
||||||
if (members) {
|
|
||||||
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
|
||||||
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
|
||||||
if (!nameEvent || !nameEvent.getContent().name) {
|
|
||||||
settingsHint = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let oobName = _t("Join Room");
|
|
||||||
if (this.props.oobData && this.props.oobData.name) {
|
|
||||||
oobName = this.props.oobData.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
let contextMenu: JSX.Element;
|
|
||||||
if (this.state.contextMenuPosition && this.props.room) {
|
|
||||||
contextMenu = (
|
|
||||||
<RoomContextMenu
|
|
||||||
{...contextMenuBelow(this.state.contextMenuPosition)}
|
|
||||||
room={this.props.room}
|
|
||||||
onFinished={this.onContextMenuCloseClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
|
||||||
const name = (
|
|
||||||
<ContextMenuTooltipButton
|
|
||||||
className="mx_RoomHeader_name"
|
|
||||||
onClick={this.onContextMenuOpenClick}
|
|
||||||
isExpanded={!!this.state.contextMenuPosition}
|
|
||||||
title={_t("Room options")}
|
|
||||||
>
|
|
||||||
<RoomName room={this.props.room}>
|
|
||||||
{ (name) => {
|
|
||||||
const roomName = name || oobName;
|
|
||||||
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
|
|
||||||
} }
|
|
||||||
</RoomName>
|
|
||||||
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
|
||||||
{ contextMenu }
|
|
||||||
</ContextMenuTooltipButton>
|
|
||||||
);
|
|
||||||
|
|
||||||
const topicElement = <RoomTopic
|
|
||||||
room={this.props.room}
|
|
||||||
className="mx_RoomHeader_topic"
|
|
||||||
/>;
|
|
||||||
|
|
||||||
let roomAvatar;
|
|
||||||
if (this.props.room) {
|
|
||||||
roomAvatar = <DecoratedRoomAvatar
|
|
||||||
room={this.props.room}
|
|
||||||
avatarSize={24}
|
|
||||||
oobData={this.props.oobData}
|
|
||||||
viewAvatarOnClick={true}
|
|
||||||
/>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const buttons: JSX.Element[] = [];
|
const buttons: JSX.Element[] = [];
|
||||||
|
|
||||||
if (this.props.inRoom &&
|
if (this.props.inRoom &&
|
||||||
|
@ -269,10 +199,105 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
buttons.push(inviteButton);
|
buttons.push(inviteButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rightRow =
|
return buttons;
|
||||||
<div className="mx_RoomHeader_buttons">
|
}
|
||||||
{ buttons }
|
|
||||||
|
private renderName(oobName) {
|
||||||
|
let contextMenu: JSX.Element;
|
||||||
|
if (this.state.contextMenuPosition && this.props.room) {
|
||||||
|
contextMenu = (
|
||||||
|
<RoomContextMenu
|
||||||
|
{...contextMenuBelow(this.state.contextMenuPosition)}
|
||||||
|
room={this.props.room}
|
||||||
|
onFinished={this.onContextMenuCloseClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: this is a bit inefficient - we could just compare room.name for 'Empty room'...
|
||||||
|
let settingsHint = false;
|
||||||
|
const members = this.props.room ? this.props.room.getJoinedMembers() : undefined;
|
||||||
|
if (members) {
|
||||||
|
if (members.length === 1 && members[0].userId === MatrixClientPeg.get().credentials.userId) {
|
||||||
|
const nameEvent = this.props.room.currentState.getStateEvents('m.room.name', '');
|
||||||
|
if (!nameEvent || !nameEvent.getContent().name) {
|
||||||
|
settingsHint = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const textClasses = classNames('mx_RoomHeader_nametext', { mx_RoomHeader_settingsHint: settingsHint });
|
||||||
|
const roomName = <RoomName room={this.props.room}>
|
||||||
|
{ (name) => {
|
||||||
|
const roomName = name || oobName;
|
||||||
|
return <div dir="auto" className={textClasses} title={roomName}>{ roomName }</div>;
|
||||||
|
} }
|
||||||
|
</RoomName>;
|
||||||
|
|
||||||
|
if (this.props.enableRoomOptionsMenu) {
|
||||||
|
return (
|
||||||
|
<ContextMenuTooltipButton
|
||||||
|
className="mx_RoomHeader_name"
|
||||||
|
onClick={this.onContextMenuOpenClick}
|
||||||
|
isExpanded={!!this.state.contextMenuPosition}
|
||||||
|
title={_t("Room options")}
|
||||||
|
>
|
||||||
|
{ roomName }
|
||||||
|
{ this.props.room && <div className="mx_RoomHeader_chevron" /> }
|
||||||
|
{ contextMenu }
|
||||||
|
</ContextMenuTooltipButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="mx_RoomHeader_name mx_RoomHeader_name--textonly">
|
||||||
|
{ roomName }
|
||||||
</div>;
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
let searchStatus = null;
|
||||||
|
|
||||||
|
// don't display the search count until the search completes and
|
||||||
|
// gives us a valid (possibly zero) searchCount.
|
||||||
|
if (this.props.searchInfo &&
|
||||||
|
this.props.searchInfo.searchCount !== undefined &&
|
||||||
|
this.props.searchInfo.searchCount !== null) {
|
||||||
|
searchStatus = <div className="mx_RoomHeader_searchStatus">
|
||||||
|
{ _t("(~%(count)s results)", { count: this.props.searchInfo.searchCount }) }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let oobName = _t("Join Room");
|
||||||
|
if (this.props.oobData && this.props.oobData.name) {
|
||||||
|
oobName = this.props.oobData.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = this.renderName(oobName);
|
||||||
|
|
||||||
|
const topicElement = <RoomTopic
|
||||||
|
room={this.props.room}
|
||||||
|
className="mx_RoomHeader_topic"
|
||||||
|
/>;
|
||||||
|
|
||||||
|
let roomAvatar;
|
||||||
|
if (this.props.room) {
|
||||||
|
roomAvatar = <DecoratedRoomAvatar
|
||||||
|
room={this.props.room}
|
||||||
|
avatarSize={24}
|
||||||
|
oobData={this.props.oobData}
|
||||||
|
viewAvatarOnClick={true}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let buttons;
|
||||||
|
if (this.props.showButtons) {
|
||||||
|
buttons = <React.Fragment>
|
||||||
|
<div className="mx_RoomHeader_buttons">
|
||||||
|
{ this.renderButtons() }
|
||||||
|
</div>
|
||||||
|
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
||||||
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
|
||||||
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
const e2eIcon = this.props.e2eStatus ? <E2EIcon status={this.props.e2eStatus} /> : undefined;
|
||||||
|
|
||||||
|
@ -294,8 +319,7 @@ export default class RoomHeader extends React.Component<IProps, IState> {
|
||||||
{ searchStatus }
|
{ searchStatus }
|
||||||
{ topicElement }
|
{ topicElement }
|
||||||
{ betaPill }
|
{ betaPill }
|
||||||
{ rightRow }
|
{ buttons }
|
||||||
<RoomHeaderButtons room={this.props.room} excludedRightPanelPhaseButtons={this.props.excludedRightPanelPhaseButtons} />
|
|
||||||
</div>
|
</div>
|
||||||
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
<RoomLiveShareWarning roomId={this.props.room.roomId} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1730,8 +1730,9 @@
|
||||||
"Code block": "Code block",
|
"Code block": "Code block",
|
||||||
"Quote": "Quote",
|
"Quote": "Quote",
|
||||||
"Insert link": "Insert link",
|
"Insert link": "Insert link",
|
||||||
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
|
||||||
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
"This is the beginning of your direct message history with <displayName/>.": "This is the beginning of your direct message history with <displayName/>.",
|
||||||
|
"Send your first message to invite <displayName/> to chat": "Send your first message to invite <displayName/> to chat",
|
||||||
|
"Only the two of you are in this conversation, unless either of you invites anyone to join.": "Only the two of you are in this conversation, unless either of you invites anyone to join.",
|
||||||
"Topic: %(topic)s (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
|
"Topic: %(topic)s (<a>edit</a>)": "Topic: %(topic)s (<a>edit</a>)",
|
||||||
"Topic: %(topic)s ": "Topic: %(topic)s ",
|
"Topic: %(topic)s ": "Topic: %(topic)s ",
|
||||||
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
|
"<a>Add a topic</a> to help people know what it is about.": "<a>Add a topic</a> to help people know what it is about.",
|
||||||
|
@ -1771,15 +1772,15 @@
|
||||||
"Room %(name)s": "Room %(name)s",
|
"Room %(name)s": "Room %(name)s",
|
||||||
"Recently visited rooms": "Recently visited rooms",
|
"Recently visited rooms": "Recently visited rooms",
|
||||||
"No recently visited rooms": "No recently visited rooms",
|
"No recently visited rooms": "No recently visited rooms",
|
||||||
"(~%(count)s results)|other": "(~%(count)s results)",
|
|
||||||
"(~%(count)s results)|one": "(~%(count)s result)",
|
|
||||||
"Join Room": "Join Room",
|
|
||||||
"Room options": "Room options",
|
|
||||||
"Forget room": "Forget room",
|
"Forget room": "Forget room",
|
||||||
"Hide Widgets": "Hide Widgets",
|
"Hide Widgets": "Hide Widgets",
|
||||||
"Show Widgets": "Show Widgets",
|
"Show Widgets": "Show Widgets",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Invite": "Invite",
|
"Invite": "Invite",
|
||||||
|
"Room options": "Room options",
|
||||||
|
"(~%(count)s results)|other": "(~%(count)s results)",
|
||||||
|
"(~%(count)s results)|one": "(~%(count)s result)",
|
||||||
|
"Join Room": "Join Room",
|
||||||
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
"Video rooms are a beta feature": "Video rooms are a beta feature",
|
||||||
"Video room": "Video room",
|
"Video room": "Video room",
|
||||||
"Public space": "Public space",
|
"Public space": "Public space",
|
||||||
|
@ -2102,6 +2103,7 @@
|
||||||
"View Source": "View Source",
|
"View Source": "View Source",
|
||||||
"Some encryption parameters have been changed.": "Some encryption parameters have been changed.",
|
"Some encryption parameters have been changed.": "Some encryption parameters have been changed.",
|
||||||
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
|
"Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.": "Messages here are end-to-end encrypted. Verify %(displayName)s in their profile - tap on their avatar.",
|
||||||
|
"Messages in this chat will be end-to-end encrypted.": "Messages in this chat will be end-to-end encrypted.",
|
||||||
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
|
"Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.": "Messages in this room are end-to-end encrypted. When people join, you can verify them in their profile, just tap on their avatar.",
|
||||||
"Encryption enabled": "Encryption enabled",
|
"Encryption enabled": "Encryption enabled",
|
||||||
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
"Ignored attempt to disable encryption": "Ignored attempt to disable encryption",
|
||||||
|
|
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import { MatrixClientPeg } from "../MatrixClientPeg";
|
import { MatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
import { isLocalRoom } from "../utils/localRoom/isLocalRoom";
|
||||||
import Timer from "../utils/Timer";
|
import Timer from "../utils/Timer";
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000;
|
const TYPING_USER_TIMEOUT = 10000;
|
||||||
|
@ -64,6 +65,9 @@ export default class TypingStore {
|
||||||
* @param {boolean} isTyping Whether the user is typing or not.
|
* @param {boolean} isTyping Whether the user is typing or not.
|
||||||
*/
|
*/
|
||||||
public setSelfTyping(roomId: string, threadId: string | null, isTyping: boolean): void {
|
public setSelfTyping(roomId: string, threadId: string | null, isTyping: boolean): void {
|
||||||
|
// No typing notifications for local rooms
|
||||||
|
if (isLocalRoom(roomId)) return;
|
||||||
|
|
||||||
if (!SettingsStore.getValue('sendTypingNotifications')) return;
|
if (!SettingsStore.getValue('sendTypingNotifications')) return;
|
||||||
if (SettingsStore.getValue('lowBandwidth')) return;
|
if (SettingsStore.getValue('lowBandwidth')) return;
|
||||||
// Disable typing notification for threads for the initial launch
|
// Disable typing notification for threads for the initial launch
|
||||||
|
|
|
@ -18,7 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import CallHandler from "../../../CallHandler";
|
import CallHandler from "../../../CallHandler";
|
||||||
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
import { RoomListCustomisations } from "../../../customisations/RoomList";
|
||||||
import { LocalRoom } from "../../../models/LocalRoom";
|
import { isLocalRoom } from "../../../utils/localRoom/isLocalRoom";
|
||||||
import VoipUserMapper from "../../../VoipUserMapper";
|
import VoipUserMapper from "../../../VoipUserMapper";
|
||||||
|
|
||||||
export class VisibilityProvider {
|
export class VisibilityProvider {
|
||||||
|
@ -55,7 +55,7 @@ export class VisibilityProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room instanceof LocalRoom) {
|
if (isLocalRoom(room)) {
|
||||||
// local rooms shouldn't show up anywhere
|
// local rooms shouldn't show up anywhere
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
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";
|
||||||
|
|
||||||
|
// Wrap DOM event handlers with stopPropagation and preventDefault
|
||||||
|
export const preventDefaultWrapper =
|
||||||
|
<T extends React.BaseSyntheticEvent = React.BaseSyntheticEvent>(callback: () => void) => (e?: T) => {
|
||||||
|
e?.stopPropagation();
|
||||||
|
e?.preventDefault();
|
||||||
|
callback();
|
||||||
|
};
|
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
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 { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
|
||||||
|
|
||||||
|
export function isLocalRoom(roomOrID: Room|string): boolean {
|
||||||
|
if (typeof roomOrID === "string") {
|
||||||
|
return roomOrID.startsWith(LOCAL_ROOM_ID_PREFIX);
|
||||||
|
}
|
||||||
|
return roomOrID instanceof LocalRoom;
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ export const useMap = ({
|
||||||
interactive,
|
interactive,
|
||||||
bodyId,
|
bodyId,
|
||||||
onError,
|
onError,
|
||||||
}: UseMapProps): MapLibreMap => {
|
}: UseMapProps): MapLibreMap | undefined => {
|
||||||
const [map, setMap] = useState<MapLibreMap>();
|
const [map, setMap] = useState<MapLibreMap>();
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
import { Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { avatarUrlForRoom } from "../src/Avatar";
|
||||||
|
import { Media, mediaFromMxc } from "../src/customisations/Media";
|
||||||
|
import DMRoomMap from "../src/utils/DMRoomMap";
|
||||||
|
|
||||||
|
jest.mock("../src/customisations/Media", () => ({
|
||||||
|
mediaFromMxc: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const roomId = "!room:example.com";
|
||||||
|
const avatarUrl1 = "https://example.com/avatar1";
|
||||||
|
const avatarUrl2 = "https://example.com/avatar2";
|
||||||
|
|
||||||
|
describe("avatarUrlForRoom", () => {
|
||||||
|
let getThumbnailOfSourceHttp: jest.Mock;
|
||||||
|
let room: Room;
|
||||||
|
let roomMember: RoomMember;
|
||||||
|
let dmRoomMap: DMRoomMap;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
getThumbnailOfSourceHttp = jest.fn();
|
||||||
|
mocked(mediaFromMxc).mockImplementation((): Media => {
|
||||||
|
return {
|
||||||
|
getThumbnailOfSourceHttp,
|
||||||
|
} as unknown as Media;
|
||||||
|
});
|
||||||
|
room = {
|
||||||
|
roomId,
|
||||||
|
getMxcAvatarUrl: jest.fn(),
|
||||||
|
isSpaceRoom: jest.fn(),
|
||||||
|
getAvatarFallbackMember: jest.fn(),
|
||||||
|
} as unknown as Room;
|
||||||
|
dmRoomMap = {
|
||||||
|
getUserIdForRoomId: jest.fn(),
|
||||||
|
} as unknown as DMRoomMap;
|
||||||
|
DMRoomMap.setShared(dmRoomMap);
|
||||||
|
roomMember = {
|
||||||
|
getMxcAvatarUrl: jest.fn(),
|
||||||
|
} as unknown as RoomMember;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for a null room", () => {
|
||||||
|
expect(avatarUrlForRoom(null, 128, 128)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the HTTP source if the room provides a MXC url", () => {
|
||||||
|
mocked(room.getMxcAvatarUrl).mockReturnValue(avatarUrl1);
|
||||||
|
getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
|
||||||
|
expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
|
||||||
|
expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for a space room", () => {
|
||||||
|
mocked(room.isSpaceRoom).mockReturnValue(true);
|
||||||
|
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if the room is not a DM", () => {
|
||||||
|
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue(null);
|
||||||
|
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||||
|
expect(dmRoomMap.getUserIdForRoomId).toHaveBeenCalledWith(roomId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if there is no other member in the room", () => {
|
||||||
|
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
|
||||||
|
mocked(room.getAvatarFallbackMember).mockReturnValue(null);
|
||||||
|
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if the other member has no avatar URL", () => {
|
||||||
|
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
|
||||||
|
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
|
||||||
|
expect(avatarUrlForRoom(room, 128, 128)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return the other member's avatar URL", () => {
|
||||||
|
mocked(dmRoomMap).getUserIdForRoomId.mockReturnValue("@user:example.com");
|
||||||
|
mocked(room.getAvatarFallbackMember).mockReturnValue(roomMember);
|
||||||
|
mocked(roomMember.getMxcAvatarUrl).mockReturnValue(avatarUrl2);
|
||||||
|
getThumbnailOfSourceHttp.mockReturnValue(avatarUrl2);
|
||||||
|
expect(avatarUrlForRoom(room, 128, 256, "crop")).toEqual(avatarUrl2);
|
||||||
|
expect(getThumbnailOfSourceHttp).toHaveBeenCalledWith(128, 256, "crop");
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,119 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
import {
|
||||||
|
MatrixEvent,
|
||||||
|
EventType,
|
||||||
|
MsgType,
|
||||||
|
} from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { haveRendererForEvent } from "../src/events/EventTileFactory";
|
||||||
|
import { getMockClientWithEventEmitter, makeBeaconEvent, mockClientMethodsUser } from "./test-utils";
|
||||||
|
import { eventTriggersUnreadCount } from "../src/Unread";
|
||||||
|
|
||||||
|
jest.mock("../src/events/EventTileFactory", () => ({
|
||||||
|
haveRendererForEvent: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('eventTriggersUnreadCount()', () => {
|
||||||
|
const aliceId = '@alice:server.org';
|
||||||
|
const bobId = '@bob:server.org';
|
||||||
|
|
||||||
|
// mock user credentials
|
||||||
|
getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(bobId),
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup events
|
||||||
|
const alicesMessage = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: aliceId,
|
||||||
|
content: {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: 'Hello from Alice',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const bobsMessage = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: bobId,
|
||||||
|
content: {
|
||||||
|
msgtype: MsgType.Text,
|
||||||
|
body: 'Hello from Bob',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const redactedEvent = new MatrixEvent({
|
||||||
|
type: EventType.RoomMessage,
|
||||||
|
sender: aliceId,
|
||||||
|
});
|
||||||
|
redactedEvent.makeRedacted(redactedEvent);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
mocked(haveRendererForEvent).mockClear().mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false when the event was sent by the current user', () => {
|
||||||
|
expect(eventTriggersUnreadCount(bobsMessage)).toBe(false);
|
||||||
|
// returned early before checking renderer
|
||||||
|
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for a redacted event', () => {
|
||||||
|
expect(eventTriggersUnreadCount(redactedEvent)).toBe(false);
|
||||||
|
// returned early before checking renderer
|
||||||
|
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for an event without a renderer', () => {
|
||||||
|
mocked(haveRendererForEvent).mockReturnValue(false);
|
||||||
|
expect(eventTriggersUnreadCount(alicesMessage)).toBe(false);
|
||||||
|
expect(haveRendererForEvent).toHaveBeenCalledWith(alicesMessage, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns true for an event with a renderer', () => {
|
||||||
|
mocked(haveRendererForEvent).mockReturnValue(true);
|
||||||
|
expect(eventTriggersUnreadCount(alicesMessage)).toBe(true);
|
||||||
|
expect(haveRendererForEvent).toHaveBeenCalledWith(alicesMessage, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns false for beacon locations', () => {
|
||||||
|
const beaconLocationEvent = makeBeaconEvent(aliceId);
|
||||||
|
expect(eventTriggersUnreadCount(beaconLocationEvent)).toBe(false);
|
||||||
|
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
const noUnreadEventTypes = [
|
||||||
|
EventType.RoomMember,
|
||||||
|
EventType.RoomThirdPartyInvite,
|
||||||
|
EventType.CallAnswer,
|
||||||
|
EventType.CallHangup,
|
||||||
|
EventType.RoomAliases,
|
||||||
|
EventType.RoomCanonicalAlias,
|
||||||
|
EventType.RoomServerAcl,
|
||||||
|
];
|
||||||
|
|
||||||
|
it.each(noUnreadEventTypes)('returns false without checking for renderer for events with type %s', (eventType) => {
|
||||||
|
const event = new MatrixEvent({
|
||||||
|
type: eventType,
|
||||||
|
sender: aliceId,
|
||||||
|
});
|
||||||
|
expect(eventTriggersUnreadCount(event)).toBe(false);
|
||||||
|
expect(haveRendererForEvent).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -16,6 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
import EmojiProvider from '../../src/autocomplete/EmojiProvider';
|
import EmojiProvider from '../../src/autocomplete/EmojiProvider';
|
||||||
import { mkStubRoom } from '../test-utils/test-utils';
|
import { mkStubRoom } from '../test-utils/test-utils';
|
||||||
|
import { add } from "../../src/emojipicker/recent";
|
||||||
|
import { stubClient } from "../test-utils";
|
||||||
|
import { MatrixClientPeg } from '../../src/MatrixClientPeg';
|
||||||
|
|
||||||
const EMOJI_SHORTCODES = [
|
const EMOJI_SHORTCODES = [
|
||||||
":+1",
|
":+1",
|
||||||
|
@ -42,6 +45,8 @@ const TOO_SHORT_EMOJI_SHORTCODE = [
|
||||||
|
|
||||||
describe('EmojiProvider', function() {
|
describe('EmojiProvider', function() {
|
||||||
const testRoom = mkStubRoom(undefined, undefined, undefined);
|
const testRoom = mkStubRoom(undefined, undefined, undefined);
|
||||||
|
stubClient();
|
||||||
|
MatrixClientPeg.get();
|
||||||
|
|
||||||
it.each(EMOJI_SHORTCODES)('Returns consistent results after final colon %s', async function(emojiShortcode) {
|
it.each(EMOJI_SHORTCODES)('Returns consistent results after final colon %s', async function(emojiShortcode) {
|
||||||
const ep = new EmojiProvider(testRoom);
|
const ep = new EmojiProvider(testRoom);
|
||||||
|
@ -64,4 +69,21 @@ describe('EmojiProvider', function() {
|
||||||
|
|
||||||
expect(completions[0].completion).toEqual(expectedEmoji);
|
expect(completions[0].completion).toEqual(expectedEmoji);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Returns correct autocompletion based on recently used emoji', async function() {
|
||||||
|
add("😘"); //kissing_heart
|
||||||
|
add("😘");
|
||||||
|
add("😚"); //kissing_closed_eyes
|
||||||
|
const emojiProvider = new EmojiProvider(null);
|
||||||
|
|
||||||
|
let completionsList = await emojiProvider.getCompletions(":kis", { beginning: true, end: 3, start: 3 });
|
||||||
|
expect(completionsList[0].component.props.title).toEqual(":kissing_heart:");
|
||||||
|
expect(completionsList[1].component.props.title).toEqual(":kissing_closed_eyes:");
|
||||||
|
|
||||||
|
completionsList = await emojiProvider.getCompletions(":kissing_c", { beginning: true, end: 3, start: 3 });
|
||||||
|
expect(completionsList[0].component.props.title).toEqual(":kissing_closed_eyes:");
|
||||||
|
|
||||||
|
completionsList = await emojiProvider.getCompletions(":so", { beginning: true, end: 2, start: 2 });
|
||||||
|
expect(completionsList[0].component.props.title).toEqual(":sob:");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
|
||||||
|
import { RoomStatusBarUnsentMessages } from "../../../src/components/structures/RoomStatusBarUnsentMessages";
|
||||||
|
import { StaticNotificationState } from "../../../src/stores/notifications/StaticNotificationState";
|
||||||
|
|
||||||
|
describe("RoomStatusBarUnsentMessages", () => {
|
||||||
|
const title = "test title";
|
||||||
|
const description = "test description";
|
||||||
|
const buttonsText = "test buttons";
|
||||||
|
const buttons = <div>{ buttonsText }</div>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
render(
|
||||||
|
<RoomStatusBarUnsentMessages
|
||||||
|
title={title}
|
||||||
|
description={description}
|
||||||
|
buttons={buttons}
|
||||||
|
notificationState={StaticNotificationState.RED_EXCLAMATION}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the values passed as props", () => {
|
||||||
|
screen.getByText(title);
|
||||||
|
screen.getByText(description);
|
||||||
|
screen.getByText(buttonsText);
|
||||||
|
// notification state
|
||||||
|
screen.getByText("!");
|
||||||
|
});
|
||||||
|
});
|
|
@ -27,6 +27,7 @@ import { act } from 'react-dom/test-utils';
|
||||||
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||||
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
import {
|
import {
|
||||||
|
findByTestId,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makeBeaconEvent,
|
makeBeaconEvent,
|
||||||
makeBeaconInfoEvent,
|
makeBeaconInfoEvent,
|
||||||
|
@ -169,5 +170,30 @@ describe('<BeaconListItem />', () => {
|
||||||
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago');
|
expect(component.find('.mx_BeaconListItem_lastUpdated').text()).toEqual('Updated a few seconds ago');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('interactions', () => {
|
||||||
|
it('does not call onClick handler when clicking share button', () => {
|
||||||
|
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const component = getComponent({ beacon, onClick });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
findByTestId(component, 'open-location-in-osm').at(0).simulate('click');
|
||||||
|
});
|
||||||
|
expect(onClick).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls onClick handler when clicking outside of share buttons', () => {
|
||||||
|
const [beacon] = setupRoomWithBeacons([alicePinBeaconEvent], [aliceLocation1]);
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const component = getComponent({ beacon, onClick });
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click the beacon name
|
||||||
|
component.find('.mx_BeaconStatus_description').simulate('click');
|
||||||
|
});
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
import {
|
import {
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
|
@ -28,15 +28,18 @@ import maplibregl from 'maplibre-gl';
|
||||||
|
|
||||||
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
|
import BeaconViewDialog from '../../../../src/components/views/beacon/BeaconViewDialog';
|
||||||
import {
|
import {
|
||||||
|
findByAttr,
|
||||||
findByTestId,
|
findByTestId,
|
||||||
getMockClientWithEventEmitter,
|
getMockClientWithEventEmitter,
|
||||||
makeBeaconEvent,
|
makeBeaconEvent,
|
||||||
makeBeaconInfoEvent,
|
makeBeaconInfoEvent,
|
||||||
|
makeRoomWithBeacons,
|
||||||
makeRoomWithStateEvents,
|
makeRoomWithStateEvents,
|
||||||
} from '../../../test-utils';
|
} from '../../../test-utils';
|
||||||
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
|
import { TILE_SERVER_WK_KEY } from '../../../../src/utils/WellKnownUtils';
|
||||||
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
import { OwnBeaconStore } from '../../../../src/stores/OwnBeaconStore';
|
||||||
import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus';
|
import { BeaconDisplayStatus } from '../../../../src/components/views/beacon/displayStatus';
|
||||||
|
import BeaconListItem from '../../../../src/components/views/beacon/BeaconListItem';
|
||||||
|
|
||||||
describe('<BeaconViewDialog />', () => {
|
describe('<BeaconViewDialog />', () => {
|
||||||
// 14.03.2022 16:15
|
// 14.03.2022 16:15
|
||||||
|
@ -89,13 +92,18 @@ describe('<BeaconViewDialog />', () => {
|
||||||
const getComponent = (props = {}) =>
|
const getComponent = (props = {}) =>
|
||||||
mount(<BeaconViewDialog {...defaultProps} {...props} />);
|
mount(<BeaconViewDialog {...defaultProps} {...props} />);
|
||||||
|
|
||||||
|
const openSidebar = (component: ReactWrapper) => act(() => {
|
||||||
|
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
||||||
|
component.setProps({});
|
||||||
|
});
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
maplibregl.AttributionControl = jest.fn();
|
maplibregl.AttributionControl = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore();
|
jest.spyOn(OwnBeaconStore.instance, 'getLiveBeaconIds').mockRestore();
|
||||||
|
jest.spyOn(global.Date, 'now').mockReturnValue(now);
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -225,10 +233,7 @@ describe('<BeaconViewDialog />', () => {
|
||||||
beacon.addLocations([location1]);
|
beacon.addLocations([location1]);
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
|
|
||||||
act(() => {
|
openSidebar(component);
|
||||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
|
||||||
component.setProps({});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component.find('DialogSidebar').length).toBeTruthy();
|
expect(component.find('DialogSidebar').length).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
@ -240,20 +245,134 @@ describe('<BeaconViewDialog />', () => {
|
||||||
const component = getComponent();
|
const component = getComponent();
|
||||||
|
|
||||||
// open the sidebar
|
// open the sidebar
|
||||||
act(() => {
|
openSidebar(component);
|
||||||
findByTestId(component, 'beacon-view-dialog-open-sidebar').at(0).simulate('click');
|
|
||||||
component.setProps({});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(component.find('DialogSidebar').length).toBeTruthy();
|
expect(component.find('DialogSidebar').length).toBeTruthy();
|
||||||
|
|
||||||
// now close it
|
// now close it
|
||||||
act(() => {
|
act(() => {
|
||||||
findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click');
|
findByAttr('data-testid')(component, 'dialog-sidebar-close').at(0).simulate('click');
|
||||||
component.setProps({});
|
component.setProps({});
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(component.find('DialogSidebar').length).toBeFalsy();
|
expect(component.find('DialogSidebar').length).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('focused beacons', () => {
|
||||||
|
const beacon2Event = makeBeaconInfoEvent(bobId,
|
||||||
|
roomId,
|
||||||
|
{ isLive: true },
|
||||||
|
'$bob-room1-2',
|
||||||
|
);
|
||||||
|
|
||||||
|
const location2 = makeBeaconEvent(
|
||||||
|
bobId, { beaconInfoId: beacon2Event.getId(), geoUri: 'geo:33,22', timestamp: now + 1 },
|
||||||
|
);
|
||||||
|
|
||||||
|
const fitBoundsOptions = { maxZoom: 15, padding: 100 };
|
||||||
|
|
||||||
|
it('opens map with both beacons in view on first load without initialFocusedBeacon', () => {
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
getComponent({ beacons: [beacon1, beacon2] });
|
||||||
|
|
||||||
|
// start centered on mid point between both beacons
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 42, lon: 31.5 });
|
||||||
|
// only called once
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||||
|
// bounds fit both beacons, only called once
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||||
|
[22, 33], [41, 51],
|
||||||
|
), fitBoundsOptions);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('opens map with both beacons in view on first load with an initially focused beacon', () => {
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
getComponent({ beacons: [beacon1, beacon2], initialFocusedBeacon: beacon1 });
|
||||||
|
|
||||||
|
// start centered on initialFocusedBeacon
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||||
|
// only called once
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||||
|
// bounds fit both beacons, only called once
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||||
|
[22, 33], [41, 51],
|
||||||
|
), fitBoundsOptions);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('focuses on beacon location on sidebar list item click', () => {
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getComponent({ beacons: [beacon1, beacon2] });
|
||||||
|
|
||||||
|
// reset call counts on map mocks after initial render
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
openSidebar(component);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click on the first beacon in the list
|
||||||
|
component.find(BeaconListItem).at(0).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
// centered on clicked beacon
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 51, lon: 41 });
|
||||||
|
// only called once
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(1);
|
||||||
|
// bounds fitted just to clicked beacon
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(new maplibregl.LngLatBounds(
|
||||||
|
[41, 51], [41, 51],
|
||||||
|
), fitBoundsOptions);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('refocuses on same beacon when clicking list item again', () => {
|
||||||
|
// test the map responds to refocusing the same beacon
|
||||||
|
const [beacon1, beacon2] = makeRoomWithBeacons(
|
||||||
|
roomId, mockClient, [defaultEvent, beacon2Event], [location1, location2],
|
||||||
|
);
|
||||||
|
|
||||||
|
const component = getComponent({ beacons: [beacon1, beacon2] });
|
||||||
|
|
||||||
|
// reset call counts on map mocks after initial render
|
||||||
|
jest.clearAllMocks();
|
||||||
|
|
||||||
|
openSidebar(component);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click on the second beacon in the list
|
||||||
|
component.find(BeaconListItem).at(1).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedBounds = new maplibregl.LngLatBounds(
|
||||||
|
[22, 33], [22, 33],
|
||||||
|
);
|
||||||
|
|
||||||
|
// date is mocked but this relies on timestamp, manually mock a tick
|
||||||
|
jest.spyOn(global.Date, 'now').mockReturnValue(now + 1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
// click on the second beacon in the list
|
||||||
|
component.find(BeaconListItem).at(1).simulate('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
// centered on clicked beacon
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledWith({ lat: 33, lon: 22 });
|
||||||
|
// bounds fitted just to clicked beacon
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledWith(expectedBounds, fitBoundsOptions);
|
||||||
|
// each called once per click
|
||||||
|
expect(mockMap.setCenter).toHaveBeenCalledTimes(2);
|
||||||
|
expect(mockMap.fitBounds).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,31 +15,88 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { mount } from 'enzyme';
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
import { act } from 'react-dom/test-utils';
|
import { act } from 'react-dom/test-utils';
|
||||||
|
|
||||||
import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar';
|
import DialogSidebar from '../../../../src/components/views/beacon/DialogSidebar';
|
||||||
import { findByTestId } from '../../../test-utils';
|
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
makeBeaconEvent,
|
||||||
|
makeBeaconInfoEvent,
|
||||||
|
makeRoomWithBeacons,
|
||||||
|
mockClientMethodsUser,
|
||||||
|
} from '../../../test-utils';
|
||||||
|
|
||||||
describe('<DialogSidebar />', () => {
|
describe('<DialogSidebar />', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
beacons: [],
|
beacons: [],
|
||||||
requestClose: jest.fn(),
|
requestClose: jest.fn(),
|
||||||
|
onBeaconClick: jest.fn(),
|
||||||
};
|
};
|
||||||
const getComponent = (props = {}) =>
|
|
||||||
mount(<DialogSidebar {...defaultProps} {...props} />);
|
|
||||||
|
|
||||||
it('renders sidebar correctly', () => {
|
const now = 1647270879403;
|
||||||
const component = getComponent();
|
|
||||||
expect(component).toMatchSnapshot();
|
const roomId = '!room:server.org';
|
||||||
|
const aliceId = '@alice:server.org';
|
||||||
|
const client = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsUser(aliceId),
|
||||||
|
getRoom: jest.fn(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const beaconEvent = makeBeaconInfoEvent(aliceId,
|
||||||
|
roomId,
|
||||||
|
{ isLive: true, timestamp: now },
|
||||||
|
'$alice-room1-1',
|
||||||
|
);
|
||||||
|
const location1 = makeBeaconEvent(
|
||||||
|
aliceId, { beaconInfoId: beaconEvent.getId(), geoUri: 'geo:51,41', timestamp: now },
|
||||||
|
);
|
||||||
|
|
||||||
|
const getComponent = (props = {}) => (
|
||||||
|
<MatrixClientContext.Provider value={client}>
|
||||||
|
<DialogSidebar {...defaultProps} {...props} />);
|
||||||
|
</MatrixClientContext.Provider>);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// mock now so time based text in snapshots is stable
|
||||||
|
jest.spyOn(Date, 'now').mockReturnValue(now);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
jest.spyOn(Date, 'now').mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders sidebar correctly without beacons', () => {
|
||||||
|
const { container } = render(getComponent());
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders sidebar correctly with beacons', () => {
|
||||||
|
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||||
|
const { container } = render(getComponent({ beacons: [beacon] }));
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls on beacon click', () => {
|
||||||
|
const onBeaconClick = jest.fn();
|
||||||
|
const [beacon] = makeRoomWithBeacons(roomId, client, [beaconEvent], [location1]);
|
||||||
|
const { container } = render(getComponent({ beacons: [beacon], onBeaconClick }));
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
const [listItem] = container.getElementsByClassName('mx_BeaconListItem');
|
||||||
|
fireEvent.click(listItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onBeaconClick).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes on close button click', () => {
|
it('closes on close button click', () => {
|
||||||
const requestClose = jest.fn();
|
const requestClose = jest.fn();
|
||||||
const component = getComponent({ requestClose });
|
const { getByTestId } = render(getComponent({ requestClose }));
|
||||||
|
|
||||||
act(() => {
|
act(() => {
|
||||||
findByTestId(component, 'dialog-sidebar-close').at(0).simulate('click');
|
fireEvent.click(getByTestId('dialog-sidebar-close'));
|
||||||
});
|
});
|
||||||
expect(requestClose).toHaveBeenCalled();
|
expect(requestClose).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
exports[`<BeaconListItem /> when a beacon is live and has locations renders beacon info 1`] = `"<li class=\\"mx_BeaconListItem\\"><div class=\\"mx_StyledLiveBeaconIcon mx_BeaconListItem_avatarIcon\\"></div><div class=\\"mx_BeaconListItem_info\\"><div class=\\"mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status\\"><div class=\\"mx_BeaconStatus_description\\"><span class=\\"mx_BeaconStatus_label\\">Alice's car</span><span class=\\"mx_BeaconStatus_expiryTime\\">Live until 16:04</span></div><div class=\\"mx_BeaconListItem_interactions\\"><div tabindex=\\"0\\"><a data-test-id=\\"open-location-in-osm\\" href=\\"https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41\\" target=\\"_blank\\" rel=\\"noreferrer noopener\\"><div class=\\"mx_ShareLatestLocation_icon\\"></div></a></div><div class=\\"mx_CopyableText mx_ShareLatestLocation_copy\\"><div aria-label=\\"Copy\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_CopyableText_copyButton\\"></div></div></div></div><span class=\\"mx_BeaconListItem_lastUpdated\\">Updated a few seconds ago</span></div></li>"`;
|
||||||
|
|
|
@ -1,53 +1,144 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<DialogSidebar /> renders sidebar correctly 1`] = `
|
exports[`<DialogSidebar /> renders sidebar correctly with beacons 1`] = `
|
||||||
<DialogSidebar
|
<div>
|
||||||
beacons={Array []}
|
<div
|
||||||
requestClose={[MockFunction]}
|
class="mx_DialogSidebar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_DialogSidebar"
|
class="mx_DialogSidebar_header"
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_DialogSidebar_header"
|
|
||||||
>
|
|
||||||
<Heading
|
|
||||||
size="h4"
|
|
||||||
>
|
>
|
||||||
<h4
|
<h4
|
||||||
className="mx_Heading_h4"
|
class="mx_Heading_h4"
|
||||||
>
|
>
|
||||||
View List
|
View List
|
||||||
</h4>
|
</h4>
|
||||||
</Heading>
|
<div
|
||||||
<AccessibleButton
|
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||||
className="mx_DialogSidebar_closeButton"
|
data-testid="dialog-sidebar-close"
|
||||||
data-test-id="dialog-sidebar-close"
|
|
||||||
element="div"
|
|
||||||
onClick={[MockFunction]}
|
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex={0}
|
tabindex="0"
|
||||||
title="Close sidebar"
|
title="Close sidebar"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
class="mx_DialogSidebar_closeButtonIcon"
|
||||||
data-test-id="dialog-sidebar-close"
|
|
||||||
onClick={[MockFunction]}
|
|
||||||
onKeyDown={[Function]}
|
|
||||||
onKeyUp={[Function]}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
title="Close sidebar"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="mx_DialogSidebar_closeButtonIcon"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
<ol
|
<ol
|
||||||
className="mx_DialogSidebar_list"
|
class="mx_DialogSidebar_list"
|
||||||
|
>
|
||||||
|
<li
|
||||||
|
class="mx_BeaconListItem"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BaseAvatar mx_BeaconListItem_avatar"
|
||||||
|
role="presentation"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_initial"
|
||||||
|
style="font-size: 20.8px; width: 32px; line-height: 32px;"
|
||||||
|
/>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
class="mx_BaseAvatar_image"
|
||||||
|
src=""
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconListItem_info"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconStatus mx_BeaconStatus_Active mx_BeaconListItem_status"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconStatus_description"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_BeaconStatus_label"
|
||||||
|
>
|
||||||
|
@alice:server.org
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="mx_BeaconStatus_expiryTime"
|
||||||
|
>
|
||||||
|
Live until 16:14
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_BeaconListItem_interactions"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
data-test-id="open-location-in-osm"
|
||||||
|
href="https://www.openstreetmap.org/?mlat=51&mlon=41#map=16/51/41"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_ShareLatestLocation_icon"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mx_CopyableText mx_ShareLatestLocation_copy"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
aria-label="Copy"
|
||||||
|
class="mx_AccessibleButton mx_CopyableText_copyButton"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</DialogSidebar>
|
</div>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="mx_BeaconListItem_lastUpdated"
|
||||||
|
>
|
||||||
|
Updated a few seconds ago
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`<DialogSidebar /> renders sidebar correctly without beacons 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="mx_DialogSidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DialogSidebar_header"
|
||||||
|
>
|
||||||
|
<h4
|
||||||
|
class="mx_Heading_h4"
|
||||||
|
>
|
||||||
|
View List
|
||||||
|
</h4>
|
||||||
|
<div
|
||||||
|
class="mx_AccessibleButton mx_DialogSidebar_closeButton"
|
||||||
|
data-testid="dialog-sidebar-close"
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
title="Close sidebar"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_DialogSidebar_closeButtonIcon"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ol
|
||||||
|
class="mx_DialogSidebar_list"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -14,8 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mount } from "enzyme";
|
import { mount, ReactWrapper } from "enzyme";
|
||||||
import { IProtocol, IPublicRoomsChunkRoom, MatrixClient, RoomMember } from "matrix-js-sdk/src/matrix";
|
import { mocked } from "jest-mock";
|
||||||
|
import { IProtocol, IPublicRoomsChunkRoom, MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { sleep } from "matrix-js-sdk/src/utils";
|
import { sleep } from "matrix-js-sdk/src/utils";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { act } from "react-dom/test-utils";
|
import { act } from "react-dom/test-utils";
|
||||||
|
@ -23,7 +24,15 @@ import sanitizeHtml from "sanitize-html";
|
||||||
|
|
||||||
import SpotlightDialog, { Filter } from "../../../../src/components/views/dialogs/spotlight/SpotlightDialog";
|
import SpotlightDialog, { Filter } from "../../../../src/components/views/dialogs/spotlight/SpotlightDialog";
|
||||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
import { stubClient } from "../../../test-utils";
|
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import { mkRoom, stubClient } from "../../../test-utils";
|
||||||
|
|
||||||
|
jest.mock("../../../../src/utils/direct-messages", () => ({
|
||||||
|
// @ts-ignore
|
||||||
|
...jest.requireActual("../../../../src/utils/direct-messages"),
|
||||||
|
startDmOnFirstMessage: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
interface IUserChunkMember {
|
interface IUserChunkMember {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
|
@ -110,10 +119,23 @@ describe("Spotlight Dialog", () => {
|
||||||
guest_can_join: false,
|
guest_can_join: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
let testRoom: Room;
|
||||||
mockClient({ rooms: [testPublicRoom], users: [testPerson] });
|
let testLocalRoom: LocalRoom;
|
||||||
});
|
|
||||||
|
|
||||||
|
let mockedClient: MatrixClient;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockedClient = mockClient({ rooms: [testPublicRoom], users: [testPerson] });
|
||||||
|
testRoom = mkRoom(mockedClient, "!test23:example.com");
|
||||||
|
mocked(testRoom.getMyMembership).mockReturnValue("join");
|
||||||
|
testLocalRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test23", mockedClient, mockedClient.getUserId());
|
||||||
|
testLocalRoom.updateMyMembership("join");
|
||||||
|
mocked(mockedClient.getVisibleRooms).mockReturnValue([testRoom, testLocalRoom]);
|
||||||
|
|
||||||
|
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
|
||||||
|
getUserIdForRoomId: jest.fn(),
|
||||||
|
} as unknown as DMRoomMap);
|
||||||
|
});
|
||||||
describe("should apply filters supplied via props", () => {
|
describe("should apply filters supplied via props", () => {
|
||||||
it("without filter", async () => {
|
it("without filter", async () => {
|
||||||
const wrapper = mount(
|
const wrapper = mount(
|
||||||
|
@ -289,4 +311,38 @@ describe("Spotlight Dialog", () => {
|
||||||
wrapper.unmount();
|
wrapper.unmount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("searching for rooms", () => {
|
||||||
|
let wrapper: ReactWrapper;
|
||||||
|
let options: ReactWrapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
wrapper = mount(
|
||||||
|
<SpotlightDialog
|
||||||
|
initialText="test23"
|
||||||
|
onFinished={() => null} />,
|
||||||
|
);
|
||||||
|
await act(async () => {
|
||||||
|
await sleep(200);
|
||||||
|
});
|
||||||
|
wrapper.update();
|
||||||
|
|
||||||
|
const content = wrapper.find("#mx_SpotlightDialog_content");
|
||||||
|
options = content.find("div.mx_SpotlightDialog_option");
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
wrapper.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should find Rooms", () => {
|
||||||
|
expect(options.length).toBe(3);
|
||||||
|
expect(options.first().text()).toContain(testRoom.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not find LocalRooms", () => {
|
||||||
|
expect(options.length).toBe(3);
|
||||||
|
expect(options.first().text()).not.toContain(testLocalRoom.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
/*
|
||||||
|
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 { mocked } from "jest-mock";
|
||||||
|
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import EncryptionEvent from "../../../../src/components/views/messages/EncryptionEvent";
|
||||||
|
import { createTestClient, mkMessage } from "../../../test-utils";
|
||||||
|
import { MatrixClientPeg } from '../../../../src/MatrixClientPeg';
|
||||||
|
import { LocalRoom } from '../../../../src/models/LocalRoom';
|
||||||
|
import DMRoomMap from '../../../../src/utils/DMRoomMap';
|
||||||
|
import MatrixClientContext from '../../../../src/contexts/MatrixClientContext';
|
||||||
|
|
||||||
|
const renderEncryptionEvent = (client: MatrixClient, event: MatrixEvent) => {
|
||||||
|
render(<MatrixClientContext.Provider value={client}>
|
||||||
|
<EncryptionEvent mxEvent={event} />
|
||||||
|
</MatrixClientContext.Provider>);
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkTexts = (title: string, subTitle: string) => {
|
||||||
|
screen.getByText(title);
|
||||||
|
screen.getByText(subTitle);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("EncryptionEvent", () => {
|
||||||
|
const roomId = "!room:example.com";
|
||||||
|
const algorithm = "m.megolm.v1.aes-sha2";
|
||||||
|
let client: MatrixClient;
|
||||||
|
let event: MatrixEvent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
client = createTestClient();
|
||||||
|
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||||
|
event = mkMessage({
|
||||||
|
event: true,
|
||||||
|
room: roomId,
|
||||||
|
user: client.getUserId(),
|
||||||
|
});
|
||||||
|
jest.spyOn(DMRoomMap, "shared").mockReturnValue({
|
||||||
|
getUserIdForRoomId: jest.fn(),
|
||||||
|
} as unknown as DMRoomMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for an encrypted room", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
event.event.content.algorithm = algorithm;
|
||||||
|
mocked(client.isRoomEncrypted).mockReturnValue(true);
|
||||||
|
const room = new Room(roomId, client, client.getUserId());
|
||||||
|
mocked(client.getRoom).mockReturnValue(room);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the expected texts", () => {
|
||||||
|
renderEncryptionEvent(client, event);
|
||||||
|
checkTexts(
|
||||||
|
"Encryption enabled",
|
||||||
|
"Messages in this room are end-to-end encrypted. "
|
||||||
|
+ "When people join, you can verify them in their profile, just tap on their avatar.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with same previous algorithm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(event, "getPrevContent").mockReturnValue({
|
||||||
|
algorithm: algorithm,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the expected texts", () => {
|
||||||
|
renderEncryptionEvent(client, event);
|
||||||
|
checkTexts(
|
||||||
|
"Encryption enabled",
|
||||||
|
"Some encryption parameters have been changed.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with unknown algorithm", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
event.event.content.algorithm = "unknown";
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the expected texts", () => {
|
||||||
|
renderEncryptionEvent(client, event);
|
||||||
|
checkTexts("Encryption enabled", "Ignored attempt to disable encryption");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for an unencrypted room", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mocked(client.isRoomEncrypted).mockReturnValue(false);
|
||||||
|
renderEncryptionEvent(client, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the expected texts", () => {
|
||||||
|
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
|
||||||
|
checkTexts("Encryption not enabled", "The encryption used by this room isn't supported.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for an encrypted local room", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
event.event.content.algorithm = algorithm;
|
||||||
|
mocked(client.isRoomEncrypted).mockReturnValue(true);
|
||||||
|
const localRoom = new LocalRoom(roomId, client, client.getUserId());
|
||||||
|
mocked(client.getRoom).mockReturnValue(localRoom);
|
||||||
|
renderEncryptionEvent(client, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should show the expected texts", () => {
|
||||||
|
expect(client.isRoomEncrypted).toHaveBeenCalledWith(roomId);
|
||||||
|
checkTexts("Encryption enabled", "Messages in this chat will be end-to-end encrypted.");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,78 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015 - 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
|
||||||
|
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 { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { LocalRoom } from "../../../../src/models/LocalRoom";
|
||||||
|
import { createTestClient } from "../../../test-utils";
|
||||||
|
import RoomContext from "../../../../src/contexts/RoomContext";
|
||||||
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
|
import NewRoomIntro from "../../../../src/components/views/rooms/NewRoomIntro";
|
||||||
|
import { IRoomState } from "../../../../src/components/structures/RoomView";
|
||||||
|
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||||
|
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||||
|
import { DirectoryMember } from "../../../../src/utils/direct-messages";
|
||||||
|
|
||||||
|
const renderNewRoomIntro = (client: MatrixClient, room: Room|LocalRoom) => {
|
||||||
|
render(
|
||||||
|
<MatrixClientContext.Provider value={client}>
|
||||||
|
<RoomContext.Provider value={{ room, roomId: room.roomId } as unknown as IRoomState}>
|
||||||
|
<NewRoomIntro />
|
||||||
|
</RoomContext.Provider>
|
||||||
|
</MatrixClientContext.Provider>,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("NewRoomIntro", () => {
|
||||||
|
let client: MatrixClient;
|
||||||
|
const roomId = "!room:example.com";
|
||||||
|
const userId = "@user:example.com";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
client = createTestClient();
|
||||||
|
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client);
|
||||||
|
DMRoomMap.makeShared();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for a DM Room", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId);
|
||||||
|
renderNewRoomIntro(client, new Room(roomId, client, client.getUserId()));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the expected intro", () => {
|
||||||
|
const expected = `This is the beginning of your direct message history with ${userId}.`;
|
||||||
|
screen.getByText((id, element) => element.tagName === "SPAN" && element.textContent === expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("for a DM LocalRoom", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(userId);
|
||||||
|
const localRoom = new LocalRoom(roomId, client, client.getUserId());
|
||||||
|
localRoom.targets.push(new DirectoryMember({ user_id: userId }));
|
||||||
|
renderNewRoomIntro(client, localRoom);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the expected intro", () => {
|
||||||
|
const expected = `Send your first message to invite ${userId} to chat`;
|
||||||
|
screen.getByText((id, element) => element.tagName === "SPAN" && element.textContent === expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -127,7 +127,7 @@ describe('RoomHeader', () => {
|
||||||
|
|
||||||
it("hides call buttons when the room is tombstoned", () => {
|
it("hides call buttons when the room is tombstoned", () => {
|
||||||
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
const wrapper = render(room, {
|
const wrapper = render(room, {}, {
|
||||||
tombstone: mkEvent({
|
tombstone: mkEvent({
|
||||||
event: true,
|
event: true,
|
||||||
type: "m.room.tombstone",
|
type: "m.room.tombstone",
|
||||||
|
@ -142,6 +142,30 @@ describe('RoomHeader', () => {
|
||||||
expect(wrapper.find('[aria-label="Voice call"]').hostNodes()).toHaveLength(0);
|
expect(wrapper.find('[aria-label="Voice call"]').hostNodes()).toHaveLength(0);
|
||||||
expect(wrapper.find('[aria-label="Video call"]').hostNodes()).toHaveLength(0);
|
expect(wrapper.find('[aria-label="Video call"]').hostNodes()).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should render buttons if not passing showButtons (default true)", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = render(room);
|
||||||
|
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render buttons if passing showButtons = false", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = render(room, { showButtons: false });
|
||||||
|
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = render(room);
|
||||||
|
expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not render the room options context menu if passing enableRoomOptionsMenu = false", () => {
|
||||||
|
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
|
||||||
|
const wrapper = render(room, { enableRoomOptionsMenu: false });
|
||||||
|
expect(wrapper.find(".mx_RoomHeader_name.mx_AccessibleButton")).toHaveLength(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IRoomCreationInfo {
|
interface IRoomCreationInfo {
|
||||||
|
@ -185,25 +209,28 @@ function createRoom(info: IRoomCreationInfo) {
|
||||||
return room;
|
return room;
|
||||||
}
|
}
|
||||||
|
|
||||||
function render(room: Room, roomContext?: Partial<IRoomState>): ReactWrapper {
|
function render(room: Room, propsOverride = {}, roomContext?: Partial<IRoomState>): ReactWrapper {
|
||||||
return mount((
|
const props = {
|
||||||
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
room,
|
||||||
<RoomHeader
|
inRoom: true,
|
||||||
room={room}
|
onSearchClick: () => {},
|
||||||
inRoom={true}
|
onInviteClick: null,
|
||||||
onSearchClick={() => {}}
|
onForgetClick: () => {},
|
||||||
onInviteClick={null}
|
onCallPlaced: (_type) => { },
|
||||||
onForgetClick={() => {}}
|
onAppsClick: () => {},
|
||||||
onCallPlaced={(_type) => { }}
|
e2eStatus: E2EStatus.Normal,
|
||||||
onAppsClick={() => {}}
|
appsShown: true,
|
||||||
e2eStatus={E2EStatus.Normal}
|
searchInfo: {
|
||||||
appsShown={true}
|
|
||||||
searchInfo={{
|
|
||||||
searchTerm: "",
|
searchTerm: "",
|
||||||
searchScope: SearchScope.Room,
|
searchScope: SearchScope.Room,
|
||||||
searchCount: 0,
|
searchCount: 0,
|
||||||
}}
|
},
|
||||||
/>
|
...propsOverride,
|
||||||
|
};
|
||||||
|
|
||||||
|
return mount((
|
||||||
|
<RoomContext.Provider value={{ ...roomContext, room } as IRoomState}>
|
||||||
|
<RoomHeader {...props} />
|
||||||
</RoomContext.Provider>
|
</RoomContext.Provider>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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 request = require('request-promise-native');
|
|
||||||
import * as cheerio from 'cheerio';
|
|
||||||
import * as url from "url";
|
|
||||||
|
|
||||||
export const approveConsent = async function(consentUrl: string): Promise<void> {
|
|
||||||
const body = await request.get(consentUrl);
|
|
||||||
const doc = cheerio.load(body);
|
|
||||||
const v = doc("input[name=v]").val();
|
|
||||||
const u = doc("input[name=u]").val();
|
|
||||||
const h = doc("input[name=h]").val();
|
|
||||||
const formAction = doc("form").attr("action");
|
|
||||||
const absAction = url.resolve(consentUrl, formAction);
|
|
||||||
await request.post(absAction).form({ v, u, h });
|
|
||||||
};
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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 request = require('request-promise-native');
|
|
||||||
import * as crypto from 'crypto';
|
|
||||||
|
|
||||||
import { RestSession } from './session';
|
|
||||||
import { RestMultiSession } from './multi';
|
|
||||||
|
|
||||||
export interface Credentials {
|
|
||||||
accessToken: string;
|
|
||||||
homeServer: string;
|
|
||||||
userId: string;
|
|
||||||
deviceId: string;
|
|
||||||
hsUrl: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RestSessionCreator {
|
|
||||||
constructor(private readonly hsUrl: string, private readonly regSecret: string) {}
|
|
||||||
|
|
||||||
public async createSessionRange(usernames: string[], password: string,
|
|
||||||
groupName: string): Promise<RestMultiSession> {
|
|
||||||
const sessionPromises = usernames.map((username) => this.createSession(username, password));
|
|
||||||
const sessions = await Promise.all(sessionPromises);
|
|
||||||
return new RestMultiSession(sessions, groupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createSession(username: string, password: string): Promise<RestSession> {
|
|
||||||
await this.register(username, password);
|
|
||||||
console.log(` * created REST user ${username} ... done`);
|
|
||||||
const authResult = await this.authenticate(username, password);
|
|
||||||
return new RestSession(authResult);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async register(username: string, password: string): Promise<void> {
|
|
||||||
// get a nonce
|
|
||||||
const regUrl = `${this.hsUrl}/_synapse/admin/v1/register`;
|
|
||||||
const nonceResp = await request.get({ uri: regUrl, json: true });
|
|
||||||
|
|
||||||
const mac = crypto.createHmac('sha1', this.regSecret).update(
|
|
||||||
`${nonceResp.nonce}\0${username}\0${password}\0notadmin`,
|
|
||||||
).digest('hex');
|
|
||||||
|
|
||||||
await request.post({
|
|
||||||
uri: regUrl,
|
|
||||||
json: true,
|
|
||||||
body: {
|
|
||||||
nonce: nonceResp.nonce,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
mac,
|
|
||||||
admin: false,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async authenticate(username: string, password: string): Promise<Credentials> {
|
|
||||||
const requestBody = {
|
|
||||||
"type": "m.login.password",
|
|
||||||
"identifier": {
|
|
||||||
"type": "m.id.user",
|
|
||||||
"user": username,
|
|
||||||
},
|
|
||||||
"password": password,
|
|
||||||
};
|
|
||||||
const url = `${this.hsUrl}/_matrix/client/r0/login`;
|
|
||||||
const responseBody = await request.post({ url, json: true, body: requestBody });
|
|
||||||
return {
|
|
||||||
accessToken: responseBody.access_token,
|
|
||||||
homeServer: responseBody.home_server,
|
|
||||||
userId: responseBody.user_id,
|
|
||||||
deviceId: responseBody.device_id,
|
|
||||||
hsUrl: this.hsUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,93 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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 { Logger } from '../logger';
|
|
||||||
import { RestSession } from "./session";
|
|
||||||
import { RestRoom } from "./room";
|
|
||||||
|
|
||||||
export class RestMultiSession {
|
|
||||||
readonly log: Logger;
|
|
||||||
|
|
||||||
constructor(public readonly sessions: RestSession[], groupName: string) {
|
|
||||||
this.log = new Logger(groupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public slice(groupName: string, start: number, end?: number): RestMultiSession {
|
|
||||||
return new RestMultiSession(this.sessions.slice(start, end), groupName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public pop(userName: string): RestSession {
|
|
||||||
const idx = this.sessions.findIndex((s) => s.userName() === userName);
|
|
||||||
if (idx === -1) {
|
|
||||||
throw new Error(`user ${userName} not found`);
|
|
||||||
}
|
|
||||||
const session = this.sessions.splice(idx, 1)[0];
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setDisplayName(fn: (s: RestSession) => string): Promise<void> {
|
|
||||||
this.log.step("set their display name");
|
|
||||||
await Promise.all(this.sessions.map(async (s: RestSession) => {
|
|
||||||
s.log.mute();
|
|
||||||
await s.setDisplayName(fn(s));
|
|
||||||
s.log.unmute();
|
|
||||||
}));
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async join(roomIdOrAlias: string): Promise<RestMultiRoom> {
|
|
||||||
this.log.step(`join ${roomIdOrAlias}`);
|
|
||||||
const rooms = await Promise.all(this.sessions.map(async (s) => {
|
|
||||||
s.log.mute();
|
|
||||||
const room = await s.join(roomIdOrAlias);
|
|
||||||
s.log.unmute();
|
|
||||||
return room;
|
|
||||||
}));
|
|
||||||
this.log.done();
|
|
||||||
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
public room(roomIdOrAlias: string): RestMultiRoom {
|
|
||||||
const rooms = this.sessions.map(s => s.room(roomIdOrAlias));
|
|
||||||
return new RestMultiRoom(rooms, roomIdOrAlias, this.log);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RestMultiRoom {
|
|
||||||
constructor(public readonly rooms: RestRoom[], private readonly roomIdOrAlias: string,
|
|
||||||
private readonly log: Logger) {}
|
|
||||||
|
|
||||||
public async talk(message: string): Promise<void> {
|
|
||||||
this.log.step(`say "${message}" in ${this.roomIdOrAlias}`);
|
|
||||||
await Promise.all(this.rooms.map(async (r: RestRoom) => {
|
|
||||||
r.log.mute();
|
|
||||||
await r.talk(message);
|
|
||||||
r.log.unmute();
|
|
||||||
}));
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async leave() {
|
|
||||||
this.log.step(`leave ${this.roomIdOrAlias}`);
|
|
||||||
await Promise.all(this.rooms.map(async (r) => {
|
|
||||||
r.log.mute();
|
|
||||||
await r.leave();
|
|
||||||
r.log.unmute();
|
|
||||||
}));
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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 uuidv4 = require('uuid/v4');
|
|
||||||
|
|
||||||
import { RestSession } from "./session";
|
|
||||||
import { Logger } from "../logger";
|
|
||||||
|
|
||||||
/* no pun intended */
|
|
||||||
export class RestRoom {
|
|
||||||
constructor(readonly session: RestSession, readonly roomId: string, readonly log: Logger) {}
|
|
||||||
|
|
||||||
async talk(message: string): Promise<string> {
|
|
||||||
this.log.step(`says "${message}" in ${this.roomId}`);
|
|
||||||
const txId = uuidv4();
|
|
||||||
const { event_id: eventId } = await this.session.put(`/rooms/${this.roomId}/send/m.room.message/${txId}`, {
|
|
||||||
"msgtype": "m.text",
|
|
||||||
"body": message,
|
|
||||||
});
|
|
||||||
this.log.done();
|
|
||||||
return eventId;
|
|
||||||
}
|
|
||||||
|
|
||||||
async leave(): Promise<void> {
|
|
||||||
this.log.step(`leaves ${this.roomId}`);
|
|
||||||
await this.session.post(`/rooms/${this.roomId}/leave`);
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,138 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
|
|
||||||
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 request = require('request-promise-native');
|
|
||||||
|
|
||||||
import { Logger } from '../logger';
|
|
||||||
import { RestRoom } from './room';
|
|
||||||
import { approveConsent } from './consent';
|
|
||||||
import { Credentials } from "./creator";
|
|
||||||
|
|
||||||
interface RoomOptions {
|
|
||||||
invite?: string;
|
|
||||||
public?: boolean;
|
|
||||||
topic?: string;
|
|
||||||
dm?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class RestSession {
|
|
||||||
private _displayName: string = null;
|
|
||||||
private readonly rooms: Record<string, RestRoom> = {};
|
|
||||||
readonly log: Logger;
|
|
||||||
|
|
||||||
constructor(private readonly credentials: Credentials) {
|
|
||||||
this.log = new Logger(credentials.userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
userId(): string {
|
|
||||||
return this.credentials.userId;
|
|
||||||
}
|
|
||||||
|
|
||||||
userName(): string {
|
|
||||||
return this.credentials.userId.split(":")[0].slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
displayName(): string {
|
|
||||||
return this._displayName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async setDisplayName(displayName: string): Promise<void> {
|
|
||||||
this.log.step(`sets their display name to ${displayName}`);
|
|
||||||
this._displayName = displayName;
|
|
||||||
await this.put(`/profile/${this.credentials.userId}/displayname`, {
|
|
||||||
displayname: displayName,
|
|
||||||
});
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
async join(roomIdOrAlias: string): Promise<RestRoom> {
|
|
||||||
this.log.step(`joins ${roomIdOrAlias}`);
|
|
||||||
const roomId = (await this.post(`/join/${encodeURIComponent(roomIdOrAlias)}`)).room_id;
|
|
||||||
this.log.done();
|
|
||||||
const room = new RestRoom(this, roomId, this.log);
|
|
||||||
this.rooms[roomId] = room;
|
|
||||||
this.rooms[roomIdOrAlias] = room;
|
|
||||||
return room;
|
|
||||||
}
|
|
||||||
|
|
||||||
room(roomIdOrAlias: string): RestRoom {
|
|
||||||
if (this.rooms.hasOwnProperty(roomIdOrAlias)) {
|
|
||||||
return this.rooms[roomIdOrAlias];
|
|
||||||
} else {
|
|
||||||
throw new Error(`${this.credentials.userId} is not in ${roomIdOrAlias}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async createRoom(name: string, options: RoomOptions): Promise<RestRoom> {
|
|
||||||
this.log.step(`creates room ${name}`);
|
|
||||||
const body = {
|
|
||||||
name,
|
|
||||||
};
|
|
||||||
if (options.invite) {
|
|
||||||
body['invite'] = options.invite;
|
|
||||||
}
|
|
||||||
if (options.public) {
|
|
||||||
body['visibility'] = "public";
|
|
||||||
} else {
|
|
||||||
body['visibility'] = "private";
|
|
||||||
}
|
|
||||||
if (options.dm) {
|
|
||||||
body['is_direct'] = true;
|
|
||||||
}
|
|
||||||
if (options.topic) {
|
|
||||||
body['topic'] = options.topic;
|
|
||||||
}
|
|
||||||
|
|
||||||
const roomId = (await this.post(`/createRoom`, body)).room_id;
|
|
||||||
this.log.done();
|
|
||||||
return new RestRoom(this, roomId, this.log);
|
|
||||||
}
|
|
||||||
|
|
||||||
post(csApiPath: string, body?: any): Promise<any> {
|
|
||||||
return this.request("POST", csApiPath, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
put(csApiPath: string, body?: any): Promise<any> {
|
|
||||||
return this.request("PUT", csApiPath, body);
|
|
||||||
}
|
|
||||||
|
|
||||||
async request(method: string, csApiPath: string, body?: any): Promise<any> {
|
|
||||||
try {
|
|
||||||
return await request({
|
|
||||||
url: `${this.credentials.hsUrl}/_matrix/client/r0${csApiPath}`,
|
|
||||||
method,
|
|
||||||
headers: {
|
|
||||||
"Authorization": `Bearer ${this.credentials.accessToken}`,
|
|
||||||
},
|
|
||||||
json: true,
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
if (!err.response) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
const responseBody = err.response.body;
|
|
||||||
if (responseBody.errcode === 'M_CONSENT_NOT_GIVEN') {
|
|
||||||
await approveConsent(responseBody.consent_uri);
|
|
||||||
return this.request(method, csApiPath, body);
|
|
||||||
} else if (responseBody && responseBody.error) {
|
|
||||||
throw new Error(`${method} ${csApiPath}: ${responseBody.error}`);
|
|
||||||
} else {
|
|
||||||
throw new Error(`${method} ${csApiPath}: ${err.response.statusCode}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,18 +15,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { range } from './util';
|
|
||||||
import { signup } from './usecases/signup';
|
import { signup } from './usecases/signup';
|
||||||
import { toastScenarios } from './scenarios/toast';
|
import { toastScenarios } from './scenarios/toast';
|
||||||
import { lazyLoadingScenarios } from './scenarios/lazy-loading';
|
|
||||||
import { e2eEncryptionScenarios } from './scenarios/e2e-encryption';
|
|
||||||
import { ElementSession } from "./session";
|
import { ElementSession } from "./session";
|
||||||
import { RestSessionCreator } from "./rest/creator";
|
|
||||||
import { RestMultiSession } from "./rest/multi";
|
|
||||||
import { RestSession } from "./rest/session";
|
|
||||||
|
|
||||||
export async function scenario(createSession: (s: string) => Promise<ElementSession>,
|
export async function scenario(createSession: (s: string) => Promise<ElementSession>): Promise<void> {
|
||||||
restCreator: RestSessionCreator): Promise<void> {
|
|
||||||
let firstUser = true;
|
let firstUser = true;
|
||||||
async function createUser(username: string) {
|
async function createUser(username: string) {
|
||||||
const session = await createSession(username);
|
const session = await createSession(username);
|
||||||
|
@ -44,15 +37,4 @@ export async function scenario(createSession: (s: string) => Promise<ElementSess
|
||||||
const bob = await createUser("bob");
|
const bob = await createUser("bob");
|
||||||
|
|
||||||
await toastScenarios(alice, bob);
|
await toastScenarios(alice, bob);
|
||||||
await e2eEncryptionScenarios(alice, bob);
|
|
||||||
console.log("create REST users:");
|
|
||||||
const charlies = await createRestUsers(restCreator);
|
|
||||||
await lazyLoadingScenarios(alice, bob, charlies);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createRestUsers(restCreator: RestSessionCreator): Promise<RestMultiSession> {
|
|
||||||
const usernames = range(1, 10).map((i) => `charly-${i}`);
|
|
||||||
const charlies = await restCreator.createSessionRange(usernames, "testtest", "charly-1..10");
|
|
||||||
await charlies.setDisplayName((s: RestSession) => `Charly #${s.userName().split('-')[1]}`);
|
|
||||||
return charlies;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { strict as assert } from 'assert';
|
|
||||||
|
|
||||||
import { ElementSession } from "../session";
|
|
||||||
import { sendMessage } from '../usecases/send-message';
|
|
||||||
import { acceptInvite } from '../usecases/accept-invite';
|
|
||||||
import { receiveMessage } from '../usecases/timeline';
|
|
||||||
import { createDm } from '../usecases/create-room';
|
|
||||||
import { checkRoomSettings } from '../usecases/room-settings';
|
|
||||||
import { startSasVerification, acceptSasVerification } from '../usecases/verify';
|
|
||||||
import { setupSecureBackup } from '../usecases/security';
|
|
||||||
import { measureStart, measureStop } from '../util';
|
|
||||||
|
|
||||||
export async function e2eEncryptionScenarios(alice: ElementSession, bob: ElementSession) {
|
|
||||||
console.log(" creating an e2e encrypted DM and join through invite:");
|
|
||||||
await createDm(bob, ['@alice:localhost']);
|
|
||||||
await checkRoomSettings(bob, { encryption: true }); // for sanity, should be e2e-by-default
|
|
||||||
await acceptInvite(alice, 'bob');
|
|
||||||
// do sas verification
|
|
||||||
bob.log.step(`starts SAS verification with ${alice.username}`);
|
|
||||||
await measureStart(bob, "mx_VerifyE2EEUser");
|
|
||||||
const bobSasPromise = startSasVerification(bob, alice.username);
|
|
||||||
const aliceSasPromise = acceptSasVerification(alice, bob.username);
|
|
||||||
// wait in parallel, so they don't deadlock on each other
|
|
||||||
// the logs get a bit messy here, but that's fine enough for debugging (hopefully)
|
|
||||||
const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]);
|
|
||||||
assert.deepEqual(bobSas, aliceSas);
|
|
||||||
await measureStop(bob, "mx_VerifyE2EEUser");
|
|
||||||
bob.log.done(`done (match for ${bobSas.join(", ")})`);
|
|
||||||
const aliceMessage = "Guess what I just heard?!";
|
|
||||||
await sendMessage(alice, aliceMessage);
|
|
||||||
await receiveMessage(bob, { sender: "alice", body: aliceMessage, encrypted: true });
|
|
||||||
const bobMessage = "You've got to tell me!";
|
|
||||||
await sendMessage(bob, bobMessage);
|
|
||||||
await receiveMessage(alice, { sender: "bob", body: bobMessage, encrypted: true });
|
|
||||||
await setupSecureBackup(alice);
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2018 New Vector Ltd
|
|
||||||
Copyright 2019 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 { strict as assert } from 'assert';
|
|
||||||
|
|
||||||
import { delay } from '../util';
|
|
||||||
import { join } from '../usecases/join';
|
|
||||||
import { sendMessage } from '../usecases/send-message';
|
|
||||||
import {
|
|
||||||
checkTimelineContains,
|
|
||||||
scrollToTimelineTop,
|
|
||||||
} from '../usecases/timeline';
|
|
||||||
import { createRoom } from '../usecases/create-room';
|
|
||||||
import { getMembersInMemberlist } from '../usecases/memberlist';
|
|
||||||
import { changeRoomSettings } from '../usecases/room-settings';
|
|
||||||
import { RestMultiSession } from "../rest/multi";
|
|
||||||
import { ElementSession } from "../session";
|
|
||||||
|
|
||||||
export async function lazyLoadingScenarios(alice: ElementSession,
|
|
||||||
bob: ElementSession, charlies: RestMultiSession): Promise<void> {
|
|
||||||
console.log(" creating a room for lazy loading member scenarios:");
|
|
||||||
const charly1to5 = charlies.slice("charly-1..5", 0, 5);
|
|
||||||
const charly6to10 = charlies.slice("charly-6..10", 5);
|
|
||||||
assert(charly1to5.sessions.length == 5);
|
|
||||||
assert(charly6to10.sessions.length == 5);
|
|
||||||
await setupRoomWithBobAliceAndCharlies(alice, bob, charly1to5);
|
|
||||||
await checkPaginatedDisplayNames(alice, charly1to5);
|
|
||||||
await checkMemberList(alice, charly1to5);
|
|
||||||
await joinCharliesWhileAliceIsOffline(alice, charly6to10);
|
|
||||||
await checkMemberList(alice, charly6to10);
|
|
||||||
await charlies.room(alias).leave();
|
|
||||||
await delay(1000);
|
|
||||||
await checkMemberListLacksCharlies(alice, charlies);
|
|
||||||
await checkMemberListLacksCharlies(bob, charlies);
|
|
||||||
}
|
|
||||||
|
|
||||||
const room = "Lazy Loading Test";
|
|
||||||
const alias = "#lltest:localhost";
|
|
||||||
const charlyMsg1 = "hi bob!";
|
|
||||||
const charlyMsg2 = "how's it going??";
|
|
||||||
|
|
||||||
async function setupRoomWithBobAliceAndCharlies(alice: ElementSession, bob: ElementSession,
|
|
||||||
charlies: RestMultiSession): Promise<void> {
|
|
||||||
await createRoom(bob, room);
|
|
||||||
await changeRoomSettings(bob, { directory: true, visibility: "public", alias });
|
|
||||||
// wait for alias to be set by server after clicking "save"
|
|
||||||
// so the charlies can join it.
|
|
||||||
await bob.delay(500);
|
|
||||||
const charlyMembers = await charlies.join(alias);
|
|
||||||
await charlyMembers.talk(charlyMsg1);
|
|
||||||
await charlyMembers.talk(charlyMsg2);
|
|
||||||
bob.log.step("sends 20 messages").mute();
|
|
||||||
for (let i = 20; i >= 1; --i) {
|
|
||||||
await sendMessage(bob, `I will only say this ${i} time(s)!`);
|
|
||||||
}
|
|
||||||
bob.log.unmute().done();
|
|
||||||
await join(alice, alias);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkPaginatedDisplayNames(alice: ElementSession, charlies: RestMultiSession): Promise<void> {
|
|
||||||
await scrollToTimelineTop(alice);
|
|
||||||
//alice should see 2 messages from every charly with
|
|
||||||
//the correct display name
|
|
||||||
const expectedMessages = [charlyMsg1, charlyMsg2].reduce((messages, msgText) => {
|
|
||||||
return charlies.sessions.reduce((messages, charly) => {
|
|
||||||
return messages.concat({
|
|
||||||
sender: charly.displayName(),
|
|
||||||
body: msgText,
|
|
||||||
});
|
|
||||||
}, messages);
|
|
||||||
}, []);
|
|
||||||
await checkTimelineContains(alice, expectedMessages, charlies.log.username);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkMemberList(alice: ElementSession, charlies: RestMultiSession): Promise<void> {
|
|
||||||
alice.log.step(`checks the memberlist contains herself, bob and ${charlies.log.username}`);
|
|
||||||
const displayNames = (await getMembersInMemberlist(alice)).map((m) => m.displayName);
|
|
||||||
assert(displayNames.includes("alice"));
|
|
||||||
assert(displayNames.includes("bob"));
|
|
||||||
charlies.sessions.forEach((charly) => {
|
|
||||||
assert(displayNames.includes(charly.displayName()),
|
|
||||||
`${charly.displayName()} should be in the member list, ` +
|
|
||||||
`only have ${displayNames}`);
|
|
||||||
});
|
|
||||||
alice.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkMemberListLacksCharlies(session: ElementSession, charlies: RestMultiSession): Promise<void> {
|
|
||||||
session.log.step(`checks the memberlist doesn't contain ${charlies.log.username}`);
|
|
||||||
const displayNames = (await getMembersInMemberlist(session)).map((m) => m.displayName);
|
|
||||||
charlies.sessions.forEach((charly) => {
|
|
||||||
assert(!displayNames.includes(charly.displayName()),
|
|
||||||
`${charly.displayName()} should not be in the member list, ` +
|
|
||||||
`only have ${displayNames}`);
|
|
||||||
});
|
|
||||||
session.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function joinCharliesWhileAliceIsOffline(alice: ElementSession, charly6to10: RestMultiSession) {
|
|
||||||
await alice.setOffline(true);
|
|
||||||
await delay(1000);
|
|
||||||
const members6to10 = await charly6to10.join(alias);
|
|
||||||
const member6 = members6to10.rooms[0];
|
|
||||||
member6.log.step("sends 20 messages").mute();
|
|
||||||
for (let i = 20; i >= 1; --i) {
|
|
||||||
await member6.talk("where is charly?");
|
|
||||||
}
|
|
||||||
member6.log.unmute().done();
|
|
||||||
const catchupPromise = alice.waitForNextSuccessfulSync();
|
|
||||||
await alice.setOffline(false);
|
|
||||||
await catchupPromise;
|
|
||||||
await delay(2000);
|
|
||||||
}
|
|
|
@ -118,24 +118,6 @@ export class ElementSession {
|
||||||
return await this.page.$$(selector);
|
return await this.page.$$(selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** wait for a /sync request started after this call that gets a 200 response */
|
|
||||||
public async waitForNextSuccessfulSync(): Promise<void> {
|
|
||||||
const syncUrls = [];
|
|
||||||
function onRequest(request) {
|
|
||||||
if (request.url().indexOf("/sync") !== -1) {
|
|
||||||
syncUrls.push(request.url());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.page.on('request', onRequest);
|
|
||||||
|
|
||||||
await this.page.waitForResponse((response) => {
|
|
||||||
return syncUrls.includes(response.request().url()) && response.status() === 200;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.page.off('request', onRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async waitNoSpinner(): Promise<void> {
|
public async waitNoSpinner(): Promise<void> {
|
||||||
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
|
await this.page.waitForSelector(".mx_Spinner", { hidden: true });
|
||||||
}
|
}
|
||||||
|
@ -152,13 +134,6 @@ export class ElementSession {
|
||||||
return delay(ms);
|
return delay(ms);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async setOffline(enabled: boolean): Promise<void> {
|
|
||||||
const description = enabled ? "offline" : "back online";
|
|
||||||
this.log.step(`goes ${description}`);
|
|
||||||
await this.page.setOfflineMode(enabled);
|
|
||||||
this.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async close(): Promise<void> {
|
public async close(): Promise<void> {
|
||||||
return this.browser.close();
|
return this.browser.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { strict as assert } from 'assert';
|
|
||||||
import { ElementHandle } from "puppeteer";
|
import { ElementHandle } from "puppeteer";
|
||||||
|
|
||||||
import { openRoomSummaryCard } from "./rightpanel";
|
import { openRoomSummaryCard } from "./rightpanel";
|
||||||
|
@ -29,46 +28,6 @@ export async function openMemberInfo(session: ElementSession, name: String): Pro
|
||||||
await matchingLabel.click();
|
await matchingLabel.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Device {
|
|
||||||
id: string;
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyDeviceForUser(session: ElementSession, name: string,
|
|
||||||
expectedDevice: Device): Promise<void> {
|
|
||||||
session.log.step(`verifies e2e device for ${name}`);
|
|
||||||
const membersAndNames = await getMembersInMemberlist(session);
|
|
||||||
const matchingLabel = membersAndNames.filter((m) => {
|
|
||||||
return m.displayName === name;
|
|
||||||
}).map((m) => m.label)[0];
|
|
||||||
await matchingLabel.click();
|
|
||||||
// click verify in member info
|
|
||||||
const firstVerifyButton = await session.query(".mx_MemberDeviceInfo_verify");
|
|
||||||
await firstVerifyButton.click();
|
|
||||||
// expect "Verify device" dialog and click "Begin Verification"
|
|
||||||
const dialogHeader = await session.innerText(await session.query(".mx_Dialog .mx_Dialog_title"));
|
|
||||||
assert(dialogHeader, "Verify device");
|
|
||||||
const beginVerificationButton = await session.query(".mx_Dialog .mx_Dialog_primary");
|
|
||||||
await beginVerificationButton.click();
|
|
||||||
// get emoji SAS labels
|
|
||||||
const sasLabelElements = await session.queryAll(
|
|
||||||
".mx_VerificationShowSas .mx_VerificationShowSas_emojiSas .mx_VerificationShowSas_emojiSas_label");
|
|
||||||
const sasLabels = await Promise.all(sasLabelElements.map(e => session.innerText(e)));
|
|
||||||
console.log("my sas labels", sasLabels);
|
|
||||||
|
|
||||||
const dialogCodeFields = await session.queryAll(".mx_QuestionDialog code");
|
|
||||||
assert.strictEqual(dialogCodeFields.length, 2);
|
|
||||||
const deviceId = await session.innerText(dialogCodeFields[0]);
|
|
||||||
const deviceKey = await session.innerText(dialogCodeFields[1]);
|
|
||||||
assert.strictEqual(expectedDevice.id, deviceId);
|
|
||||||
assert.strictEqual(expectedDevice.key, deviceKey);
|
|
||||||
const confirmButton = await session.query(".mx_Dialog_primary");
|
|
||||||
await confirmButton.click();
|
|
||||||
const closeMemberInfo = await session.query(".mx_MemberInfo_cancel");
|
|
||||||
await closeMemberInfo.click();
|
|
||||||
session.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MemberName {
|
interface MemberName {
|
||||||
label: ElementHandle;
|
label: ElementHandle;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|
|
@ -20,32 +20,6 @@ import { ElementHandle } from "puppeteer";
|
||||||
|
|
||||||
import { ElementSession } from "../session";
|
import { ElementSession } from "../session";
|
||||||
|
|
||||||
export async function scrollToTimelineTop(session: ElementSession): Promise<void> {
|
|
||||||
session.log.step(`scrolls to the top of the timeline`);
|
|
||||||
await session.page.evaluate(() => {
|
|
||||||
return Promise.resolve().then(async () => {
|
|
||||||
let timedOut = false;
|
|
||||||
let timeoutHandle = null;
|
|
||||||
// set scrollTop to 0 in a loop and check every 50ms
|
|
||||||
// if content became available (scrollTop not being 0 anymore),
|
|
||||||
// assume everything is loaded after 3s
|
|
||||||
do {
|
|
||||||
const timelineScrollView = document.querySelector(".mx_RoomView_timeline .mx_ScrollPanel");
|
|
||||||
if (timelineScrollView && timelineScrollView.scrollTop !== 0) {
|
|
||||||
if (timeoutHandle) {
|
|
||||||
clearTimeout(timeoutHandle);
|
|
||||||
}
|
|
||||||
timeoutHandle = setTimeout(() => timedOut = true, 3000);
|
|
||||||
timelineScrollView.scrollTop = 0;
|
|
||||||
} else {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
||||||
}
|
|
||||||
} while (!timedOut);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
session.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
sender: string;
|
sender: string;
|
||||||
encrypted?: boolean;
|
encrypted?: boolean;
|
||||||
|
@ -79,41 +53,6 @@ export async function receiveMessage(session: ElementSession, expectedMessage: M
|
||||||
session.log.done();
|
session.log.done();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkTimelineContains(session: ElementSession, expectedMessages: Message[],
|
|
||||||
sendersDescription: string): Promise<void> {
|
|
||||||
session.log.step(`checks timeline contains ${expectedMessages.length} ` +
|
|
||||||
`given messages${sendersDescription ? ` from ${sendersDescription}`:""}`);
|
|
||||||
const eventTiles = await getAllEventTiles(session);
|
|
||||||
let timelineMessages: Message[] = await Promise.all(eventTiles.map((eventTile) => {
|
|
||||||
return getMessageFromEventTile(eventTile);
|
|
||||||
}));
|
|
||||||
//filter out tiles that were not messages
|
|
||||||
timelineMessages = timelineMessages.filter((m) => !!m);
|
|
||||||
timelineMessages.reduce((prevSender: string, m) => {
|
|
||||||
if (m.continuation) {
|
|
||||||
m.sender = prevSender;
|
|
||||||
return prevSender;
|
|
||||||
} else {
|
|
||||||
return m.sender;
|
|
||||||
}
|
|
||||||
}, "");
|
|
||||||
|
|
||||||
expectedMessages.forEach((expectedMessage) => {
|
|
||||||
const foundMessage = timelineMessages.find((message) => {
|
|
||||||
return message.sender === expectedMessage.sender &&
|
|
||||||
message.body === expectedMessage.body;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
assertMessage(foundMessage, expectedMessage);
|
|
||||||
} catch (err) {
|
|
||||||
console.log("timelineMessages", timelineMessages);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
session.log.done();
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
|
function assertMessage(foundMessage: Message, expectedMessage: Message): void {
|
||||||
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
|
assert(foundMessage, `message ${JSON.stringify(expectedMessage)} not found in timeline`);
|
||||||
assert.equal(foundMessage.body, expectedMessage.body);
|
assert.equal(foundMessage.body, expectedMessage.body);
|
||||||
|
@ -127,10 +66,6 @@ function getLastEventTile(session: ElementSession): Promise<ElementHandle> {
|
||||||
return session.query(".mx_EventTile_last");
|
return session.query(".mx_EventTile_last");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllEventTiles(session: ElementSession): Promise<ElementHandle[]> {
|
|
||||||
return session.queryAll(".mx_RoomView_MessageList .mx_EventTile");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
|
async function getMessageFromEventTile(eventTile: ElementHandle): Promise<Message> {
|
||||||
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
|
const senderElement = await eventTile.$(".mx_DisambiguatedProfile_displayName");
|
||||||
const className: string = await (await eventTile.getProperty("className")).jsonValue();
|
const className: string = await (await eventTile.getProperty("className")).jsonValue();
|
||||||
|
|
|
@ -20,14 +20,6 @@ import { padEnd } from "lodash";
|
||||||
|
|
||||||
import { ElementSession } from "./session";
|
import { ElementSession } from "./session";
|
||||||
|
|
||||||
export const range = function(start: number, amount: number, step = 1): Array<number> {
|
|
||||||
const r = [];
|
|
||||||
for (let i = 0; i < amount; ++i) {
|
|
||||||
r.push(start + (i * step));
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const delay = function(ms: number): Promise<void> {
|
export const delay = function(ms: number): Promise<void> {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
};
|
};
|
||||||
|
|
|
@ -19,7 +19,6 @@ import { Command } from "commander";
|
||||||
|
|
||||||
import { ElementSession } from './src/session';
|
import { ElementSession } from './src/session';
|
||||||
import { scenario } from './src/scenario';
|
import { scenario } from './src/scenario';
|
||||||
import { RestSessionCreator } from './src/rest/creator';
|
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
@ -54,12 +53,7 @@ async function runTests() {
|
||||||
options['executablePath'] = path;
|
options['executablePath'] = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
const restCreator = new RestSessionCreator(
|
async function createSession(username: string) {
|
||||||
hsUrl,
|
|
||||||
program.opts().registrationSharedSecret,
|
|
||||||
);
|
|
||||||
|
|
||||||
async function createSession(username) {
|
|
||||||
const session = await ElementSession.create(
|
const session = await ElementSession.create(
|
||||||
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
|
username, options, program.opts().appUrl, hsUrl, program.opts().throttleCpu,
|
||||||
);
|
);
|
||||||
|
@ -69,7 +63,7 @@ async function runTests() {
|
||||||
|
|
||||||
let failure = false;
|
let failure = false;
|
||||||
try {
|
try {
|
||||||
await scenario(createSession, restCreator);
|
await scenario(createSession);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
failure = true;
|
failure = true;
|
||||||
console.log('failure: ', err);
|
console.log('failure: ', err);
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
/*
|
||||||
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mocked } from "jest-mock";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||||
|
import TypingStore from "../../src/stores/TypingStore";
|
||||||
|
import { LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
|
||||||
|
import SettingsStore from "../../src/settings/SettingsStore";
|
||||||
|
|
||||||
|
jest.mock("../../src/settings/SettingsStore", () => ({
|
||||||
|
getValue: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("TypingStore", () => {
|
||||||
|
let typingStore: TypingStore;
|
||||||
|
let mockClient: MatrixClient;
|
||||||
|
const settings = {
|
||||||
|
"sendTypingNotifications": true,
|
||||||
|
"feature_thread": false,
|
||||||
|
};
|
||||||
|
const roomId = "!test:example.com";
|
||||||
|
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
typingStore = new TypingStore();
|
||||||
|
mockClient = {
|
||||||
|
sendTyping: jest.fn(),
|
||||||
|
} as unknown as MatrixClient;
|
||||||
|
MatrixClientPeg.get = () => mockClient;
|
||||||
|
mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
|
||||||
|
return settings[setting];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("setSelfTyping", () => {
|
||||||
|
it("shouldn't do anything for a local room", () => {
|
||||||
|
typingStore.setSelfTyping(localRoomId, null, true);
|
||||||
|
expect(mockClient.sendTyping).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("in typing state true", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
typingStore.setSelfTyping(roomId, null, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change to false when setting false", () => {
|
||||||
|
typingStore.setSelfTyping(roomId, null, false);
|
||||||
|
expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, false, 30000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change to true when setting true", () => {
|
||||||
|
typingStore.setSelfTyping(roomId, null, true);
|
||||||
|
expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, true, 30000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("in typing state false", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
typingStore.setSelfTyping(roomId, null, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shouldn't change when setting false", () => {
|
||||||
|
typingStore.setSelfTyping(roomId, null, false);
|
||||||
|
expect(mockClient.sendTyping).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should change to true when setting true", () => {
|
||||||
|
typingStore.setSelfTyping(roomId, null, true);
|
||||||
|
expect(mockClient.sendTyping).toHaveBeenCalledWith(roomId, true, 30000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -205,7 +205,11 @@ export const makeRoomWithBeacons = (
|
||||||
const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
|
const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
|
||||||
const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event)));
|
const beacons = beaconInfoEvents.map(event => room.currentState.beacons.get(getBeaconInfoIdentifier(event)));
|
||||||
if (locationEvents) {
|
if (locationEvents) {
|
||||||
beacons.forEach(beacon => beacon.addLocations(locationEvents));
|
beacons.forEach(beacon => {
|
||||||
|
// this filtering happens in roomState, which is bypassed here
|
||||||
|
const validLocationEvents = locationEvents?.filter(event => event.getSender() === beacon.beaconInfoOwner);
|
||||||
|
beacon.addLocations(validLocationEvents);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return beacons;
|
return beacons;
|
||||||
};
|
};
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
IEventRelation,
|
IEventRelation,
|
||||||
IUnsigned,
|
IUnsigned,
|
||||||
} from 'matrix-js-sdk/src/matrix';
|
} from 'matrix-js-sdk/src/matrix';
|
||||||
|
import { normalize } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
|
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
|
||||||
import dis from '../../src/dispatcher/dispatcher';
|
import dis from '../../src/dispatcher/dispatcher';
|
||||||
|
@ -389,6 +390,7 @@ export function mkStubRoom(roomId: string = null, name: string, client: MatrixCl
|
||||||
removeListener: jest.fn(),
|
removeListener: jest.fn(),
|
||||||
getDMInviter: jest.fn(),
|
getDMInviter: jest.fn(),
|
||||||
name,
|
name,
|
||||||
|
normalizedName: normalize(name || ""),
|
||||||
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
getAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||||
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
getMxcAvatarUrl: () => 'mxc://avatar.url/room.png',
|
||||||
isSpaceRoom: jest.fn().mockReturnValue(false),
|
isSpaceRoom: jest.fn().mockReturnValue(false),
|
||||||
|
|
|
@ -35,7 +35,8 @@ export function untilDispatch(waitForAction: DispatcherAction): Promise<ActionPa
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const findByAttr = (attr: string) => (component: ReactWrapper, value: string) => component.find(`[${attr}="${value}"]`);
|
export const findByAttr = (attr: string) => (component: ReactWrapper, value: string) =>
|
||||||
|
component.find(`[${attr}="${value}"]`);
|
||||||
export const findByTestId = findByAttr('data-test-id');
|
export const findByTestId = findByAttr('data-test-id');
|
||||||
export const findById = findByAttr('id');
|
export const findById = findByAttr('id');
|
||||||
export const findByAriaLabel = findByAttr('aria-label');
|
export const findByAriaLabel = findByAttr('aria-label');
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
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 { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../src/models/LocalRoom";
|
||||||
|
import { isLocalRoom } from "../../../src/utils/localRoom/isLocalRoom";
|
||||||
|
import { createTestClient } from "../../test-utils";
|
||||||
|
|
||||||
|
describe("isLocalRoom", () => {
|
||||||
|
let room: Room;
|
||||||
|
let localRoom: LocalRoom;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
const client = createTestClient();
|
||||||
|
room = new Room("!room:example.com", client, client.getUserId());
|
||||||
|
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, client.getUserId());
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for null", () => {
|
||||||
|
expect(isLocalRoom(null)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for a Room", () => {
|
||||||
|
expect(isLocalRoom(room)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return false for a non-local room ID", () => {
|
||||||
|
expect(isLocalRoom(room.roomId)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for LocalRoom", () => {
|
||||||
|
expect(isLocalRoom(localRoom)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return true for local room ID", () => {
|
||||||
|
expect(isLocalRoom(LOCAL_ROOM_ID_PREFIX + "test")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
24
yarn.lock
24
yarn.lock
|
@ -2853,10 +2853,12 @@ balanced-match@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-2.0.0.tgz#dc70f920d78db8b858535795867bf48f820633d9"
|
||||||
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
integrity sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==
|
||||||
|
|
||||||
base-x@^4.0.0:
|
base-x@^3.0.2:
|
||||||
version "4.0.0"
|
version "3.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/base-x/-/base-x-4.0.0.tgz#d0e3b7753450c73f8ad2389b5c018a4af7b2224a"
|
resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.9.tgz#6349aaabb58526332de9f60995e548a53fe21320"
|
||||||
integrity sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==
|
integrity sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
base64-js@^1.3.1:
|
base64-js@^1.3.1:
|
||||||
version "1.5.1"
|
version "1.5.1"
|
||||||
|
@ -2974,12 +2976,12 @@ browserslist@^4.20.2, browserslist@^4.21.1:
|
||||||
node-releases "^2.0.5"
|
node-releases "^2.0.5"
|
||||||
update-browserslist-db "^1.0.4"
|
update-browserslist-db "^1.0.4"
|
||||||
|
|
||||||
bs58@^5.0.0:
|
bs58@^4.0.1:
|
||||||
version "5.0.0"
|
version "4.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/bs58/-/bs58-5.0.0.tgz#865575b4d13c09ea2a84622df6c8cbeb54ffc279"
|
resolved "https://registry.yarnpkg.com/bs58/-/bs58-4.0.1.tgz#be161e76c354f6f788ae4071f63f34e8c4f0a42a"
|
||||||
integrity sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==
|
integrity sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==
|
||||||
dependencies:
|
dependencies:
|
||||||
base-x "^4.0.0"
|
base-x "^3.0.2"
|
||||||
|
|
||||||
bser@2.1.1:
|
bser@2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
|
@ -3539,7 +3541,7 @@ csstype@^3.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.0.tgz#4ddcac3718d787cf9df0d1b7d15033925c8f29f2"
|
||||||
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
integrity sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==
|
||||||
|
|
||||||
cypress-real-events@^1.7.0:
|
cypress-real-events@^1.7.1:
|
||||||
version "1.7.1"
|
version "1.7.1"
|
||||||
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
|
resolved "https://registry.yarnpkg.com/cypress-real-events/-/cypress-real-events-1.7.1.tgz#8f430d67c29ea4f05b9c5b0311780120cbc9b935"
|
||||||
integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ==
|
integrity sha512-/Bg15RgJ0SYsuXc6lPqH08x19z6j2vmhWN4wXfJqm3z8BTAFiK2MvipZPzxT8Z0jJP0q7kuniWrLIvz/i/8lCQ==
|
||||||
|
@ -7284,7 +7286,7 @@ p-map@^4.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
aggregate-error "^3.0.0"
|
aggregate-error "^3.0.0"
|
||||||
|
|
||||||
p-retry@4:
|
p-retry@^4.5.0:
|
||||||
version "4.6.2"
|
version "4.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
|
resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16"
|
||||||
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
|
integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==
|
||||||
|
|
Loading…
Reference in New Issue