Merge branch 'develop' into johannes/webpack-5

This commit is contained in:
Johannes Marbach 2023-11-16 18:59:39 +01:00
commit d6ea051e46
40 changed files with 1956 additions and 1238 deletions

View file

@ -41,9 +41,6 @@ jobs:
outputs:
uuid: ${{ steps.uuid.outputs.value }}
pr_id: ${{ steps.prdetails.outputs.pr_id }}
commit_message: ${{ steps.commit.outputs.message }}
commit_author: ${{ steps.commit.outputs.author }}
commit_email: ${{ steps.commit.outputs.email }}
percy_enable: ${{ steps.percy.outputs.value || '0' }}
steps:
# We create the status here and then update it to success/failure in the `report` stage
@ -63,21 +60,6 @@ jobs:
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
branch: ${{ github.event.workflow_run.head_branch }}
- name: Get commit details
id: commit
if: github.event.workflow_run.event == 'pull_request' || github.event.workflow_run.event == 'merge_group'
uses: actions/github-script@v6
with:
script: |
const response = await github.rest.git.getCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: "${{ github.event.workflow_run.head_sha }}",
});
core.setOutput("message", response.data.message);
core.setOutput("author", response.data.author.name);
core.setOutput("email", response.data.author.email);
# Percy is disabled while we're figuring out https://github.com/vector-im/wat-internal/issues/36
# and https://github.com/vector-im/wat-internal/issues/56. We're hoping to turn it back on or switch
# to an alternative in the future.
@ -108,11 +90,13 @@ jobs:
strategy:
fail-fast: false
matrix:
# Naive segmentation of tests
segment: ["a-i", "j-p", "q-s", "t-z"]
# Run tests using both crypto stacks
crypto: [legacy, rust]
ci_node_total: [4]
ci_node_index: [0, 1, 2, 3]
steps:
# The version of chrome shipped by default may not be consistent across runners
# so we explicitly use a specific version of chrome here.
- uses: browser-actions/setup-chrome@803ef6dfb4fdf22089c9563225d95e4a515820a0 # v1
- run: echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV
@ -154,23 +138,24 @@ jobs:
run: |
echo "CYPRESS_RUST_CRYPTO=1" >> "$GITHUB_ENV"
- name: Run Cypress tests
- name: Run Cypress tests via knapsack pro
uses: cypress-io/github-action@ebe8b24c4428922d0f793a5c4c96853a633180e3 # v6.6.0
with:
working-directory: matrix-react-sdk
# The built-in Electron runner seems to grind to a halt trying to run the tests, so use chrome.
browser: ${{ steps.setup-chrome.outputs.chrome-path }}
headed: true
start: npx serve -p 8080 -L ../webapp
wait-on: "http://localhost:8080"
record: true
record: false
parallel: false
command-prefix: "yarn percy exec --parallel --"
ci-build-id: ${{ needs.prepare.outputs.uuid }}-${{ matrix.crypto }}
spec: cypress/e2e/[${{ matrix.segment }}]*/**
# The built-in Electron runner seems to grind to a halt trying to run the tests, so use chrome.
command: yarn percy exec --parallel -- npx knapsack-pro-cypress --config trashAssetsBeforeRuns=false --browser "$BROWSER_PATH"
env:
# pass the Dashboard record key as an environment variable
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
# Knapsack token and config
KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS: ${{ matrix.crypto == 'rust' && secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS_RUST || secrets.KNAPSACK_PRO_TEST_SUITE_TOKEN_CYPRESS_LEGACY }}
KNAPSACK_PRO_CI_NODE_TOTAL: ${{ matrix.ci_node_total }}
KNAPSACK_PRO_CI_NODE_INDEX: ${{ matrix.ci_node_index }}
KNAPSACK_PRO_TEST_FILE_PATTERN: cypress/e2e/**/*.spec.ts
KNAPSACK_PRO_BRANCH: ${{ github.event.workflow_run.head_branch }}
# Use existing chromium rather than downloading another
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
@ -181,16 +166,6 @@ jobs:
# make Node's os.tmpdir() return something where we actually have permissions
TMPDIR: ${{ runner.temp }}
# tell Cypress more details about the context of this run
COMMIT_INFO_BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT_INFO_SHA: ${{ github.event.workflow_run.head_sha }}
COMMIT_INFO_REMOTE: ${{ github.repositoryUrl }}
COMMIT_INFO_MESSAGE: ${{ needs.prepare.outputs.commit_message }}
COMMIT_INFO_AUTHOR: ${{ needs.prepare.outputs.commit_author }}
COMMIT_INFO_EMAIL: ${{ needs.prepare.outputs.commit_email }}
CYPRESS_PULL_REQUEST_ID: ${{ needs.prepare.outputs.pr_id }}
CYPRESS_PULL_REQUEST_URL: https://github.com/${{ github.repository }}/pull/${{ needs.prepare.outputs.pr_id }}
# pass the Percy token as an environment variable
PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

View file

@ -1,6 +1,8 @@
name: Localazy Download
on:
workflow_dispatch: {}
schedule:
- cron: "0 6 * * 1,3,5" # Every Monday, Wednesday and Friday at 6am UTC
jobs:
download:
uses: matrix-org/matrix-web-i18n/.github/workflows/localazy_download.yaml@main

View file

@ -2,13 +2,20 @@ name: Release Drafter
on:
push:
branches: [staging]
workflow_dispatch:
inputs:
previous-version:
description: What release to use as a base for release note purposes
required: false
type: string
concurrency: ${{ github.workflow }}
jobs:
draft:
runs-on: ubuntu-latest
steps:
- uses: release-drafter/release-drafter@dabcf3767562210392d862070ed2ef6434b9bc6f # v5
- uses: release-drafter/release-drafter@e64b19c4c46173209ed9f2e5a2f4ca7de89a0e86 # v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
disable-autolabeler: true
previous-version: ${{ inputs.previous-version }}

View file

@ -1,7 +1,8 @@
[![npm](https://img.shields.io/npm/v/matrix-react-sdk)](https://www.npmjs.com/package/matrix-react-sdk)
![Tests](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/tests.yml/badge.svg)
![Static Analysis](https://github.com/matrix-org/matrix-react-sdk/actions/workflows/static_analysis.yaml/badge.svg)
[![matrix-react-sdk](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/ppvnzg/develop&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/ppvnzg/runs)
[![Knapsack Pro Parallel CI builds for Cypress Test - Legacy Crypto](https://img.shields.io/badge/Knapsack%20Pro-Parallel%20%2F%20Cypress%20Test%20--%20Legacy%20Crypto-%230074ff)](https://knapsackpro.com/dashboard/organizations/3882/projects/2469/test_suites/3724/builds?utm_campaign=organization-id-3882&utm_content=test-suite-id-3724&utm_medium=readme&utm_source=knapsack-pro-badge&utm_term=project-id-2469)
[![Knapsack Pro Parallel CI builds for Cypress Test - Rust Crypto](https://img.shields.io/badge/Knapsack%20Pro-Parallel%20%2F%20Cypress%20Test%20--%20Rust%20Crypto-%230074ff)](https://knapsackpro.com/dashboard/organizations/3882/projects/2469/test_suites/3729/builds?utm_campaign=organization-id-3882&utm_content=test-suite-id-3729&utm_medium=readme&utm_source=knapsack-pro-badge&utm_term=project-id-2469)
<!--
Percy is disabled while we're figuring out https://github.com/vector-im/wat-internal/issues/36

View file

@ -116,7 +116,7 @@ const verify = function (this: CryptoTestContext) {
const bobsVerificationRequestPromise = waitForVerificationRequest(this.bob);
openRoomInfo().within(() => {
cy.findByRole("button", { name: /People \d/ }).click(); // \d is the number of the room members
cy.findByRole("menuitem", { name: "People" }).click();
cy.findByText("Bob").click();
cy.findByRole("button", { name: "Verify" }).click();
cy.findByRole("button", { name: "Start Verification" }).click();

View file

@ -47,16 +47,8 @@ describe("Invite dialog", function () {
// Open the room info panel
cy.findByRole("button", { name: "Room info" }).click();
cy.get(".mx_RightPanel").within(() => {
// Click "People" button on the panel
// Regex pattern due to the string of "mx_BaseCard_Button_sublabel"
cy.findByRole("button", { name: /People/ }).click();
});
cy.get(".mx_BaseCard").within(() => {
// Click "Invite to this room" button
// Regex pattern due to "mx_MemberList_invite span::before"
cy.findByRole("button", { name: /Invite to this room/ }).click();
cy.findByRole("menuitem", { name: "Invite" }).click();
});
cy.get(".mx_InviteDialog_other").within(() => {

View file

@ -121,7 +121,7 @@ describe("Lazy Loading", () => {
});
cy.get(".mx_RoomSummaryCard").within(() => {
cy.findByRole("button", { name: /People \d/ }).click(); // \d represents the number of the room members
cy.findByRole("menuitem", { name: "People" }).click(); // \d represents the number of the room members
});
}

View file

@ -77,7 +77,7 @@ describe("Poll history", () => {
function openPollHistory(): void {
cy.findByRole("button", { name: "Room info" }).click();
cy.get(".mx_RoomSummaryCard").within(() => {
cy.findByRole("button", { name: "Poll history" }).click();
cy.findByRole("menuitem", { name: "Poll history" }).click();
});
}

View file

@ -501,7 +501,7 @@ describe("Read receipts", () => {
goTo(room2);
assertReadThread("Root");
});
// XXX: fails because sometimes the room is still unread after opening the thread (initially)
// XXX: fails because it flakes - sometimes the room is still unread after opening the thread (initially)
it.skip("Reacting to a redacted message leaves the thread read", () => {
// Given a message in a thread was redacted and everything is read
goTo(room1);
@ -541,8 +541,8 @@ describe("Read receipts", () => {
// Then the room is unread
assertStillRead(room2);
});
// XXX: fails because the room still shows "1" even though we have read the thread (stuck unread)
it.skip("Reading a reaction to a redacted message marks the thread as read", () => {
// XXX: failed because flakes: https://github.com/vector-im/element-web/issues/26594
it.skip("Reading a thread after a reaction to a redacted message marks the thread as read", () => {
// Given a redacted message in a thread exists, but someone reacted to it before it was redacted
goTo(room1);
receiveMessages(room2, [
@ -555,7 +555,7 @@ describe("Read receipts", () => {
receiveMessages(room2, [redactionOf("Msg3")]);
assertUnread(room2, 2);
// When we read the thread, creating a receipt that points at the reaction
// When we read the thread
goTo(room2);
openThread("Root");

View file

@ -61,7 +61,7 @@ describe("FilePanel", () => {
// Open the file panel
viewRoomSummaryByName(ROOM_NAME);
cy.get(".mx_RoomSummaryCard_icon_files").click();
cy.findByRole("menuitem", { name: "Files" }).click();
cy.get(".mx_FilePanel").should("have.length", 1);
});

View file

@ -103,21 +103,21 @@ describe("RightPanel", () => {
it("should handle viewing export chat", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("button", { name: "Export chat" }).click();
cy.findByRole("menuitem", { name: "Export Chat" }).click();
cy.get(".mx_ExportDialog").should("have.length", 1);
});
it("should handle viewing share room", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("button", { name: "Share room" }).click();
cy.findByRole("menuitem", { name: "Copy link" }).click();
cy.get(".mx_ShareDialog").should("have.length", 1);
});
it("should handle viewing room settings", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("button", { name: "Room settings" }).click();
cy.findByRole("menuitem", { name: "Settings" }).click();
cy.get(".mx_RoomSettingsDialog").should("have.length", 1);
cy.get(".mx_Dialog_title").within(() => {
cy.findByText("Room Settings - " + ROOM_NAME).should("exist");
@ -127,7 +127,7 @@ describe("RightPanel", () => {
it("should handle viewing files", () => {
viewRoomSummaryByName(ROOM_NAME);
cy.findByRole("button", { name: "Files" }).click();
cy.findByRole("menuitem", { name: "Files" }).click();
cy.get(".mx_FilePanel").should("have.length", 1);
cy.get(".mx_FilePanel_empty").should("have.length", 1);
@ -138,8 +138,7 @@ describe("RightPanel", () => {
it("should handle viewing room member", () => {
viewRoomSummaryByName(ROOM_NAME);
// \d represents the number of the room members inside mx_BaseCard_Button_sublabel
cy.findByRole("button", { name: /People \d/ }).click();
cy.findByRole("menuitem", { name: "People" }).click();
cy.get(".mx_MemberList").should("have.length", 1);
getMemberTileByName(NAME).click();

View file

@ -61,16 +61,15 @@
},
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/analytics-events": "^0.8.0",
"@matrix-org/analytics-events": "^0.9.0",
"@matrix-org/emojibase-bindings": "^1.1.2",
"@matrix-org/matrix-wysiwyg": "2.4.1",
"@matrix-org/matrix-wysiwyg": "2.16.0",
"@matrix-org/react-sdk-module-api": "^2.2.1",
"@matrix-org/spec": "^1.7.0",
"@sentry/browser": "^7.0.0",
"@sentry/tracing": "^7.0.0",
"@testing-library/react-hooks": "^8.0.1",
"@vector-im/compound-design-tokens": "^0.0.7",
"@vector-im/compound-web": "0.6.3",
"@vector-im/compound-web": "0.8.1",
"@zxcvbn-ts/core": "^3.0.4",
"@zxcvbn-ts/language-common": "^3.0.4",
"@zxcvbn-ts/language-en": "^3.0.2",
@ -93,10 +92,10 @@
"is-ip": "^3.1.0",
"jszip": "^3.7.0",
"katex": "^0.16.0",
"linkify-element": "4.1.1",
"linkify-react": "4.1.1",
"linkify-string": "4.1.1",
"linkifyjs": "4.1.1",
"linkify-element": "4.1.2",
"linkify-react": "4.1.2",
"linkify-string": "4.1.2",
"linkifyjs": "4.1.2",
"lodash": "^4.17.20",
"maplibre-gl": "^2.0.0",
"matrix-encrypt-attachment": "^1.0.3",
@ -109,7 +108,7 @@
"opus-recorder": "^8.0.3",
"pako": "^2.0.3",
"png-chunks-extract": "^1.0.0",
"posthog-js": "1.87.2",
"posthog-js": "1.88.4",
"proposal-temporal": "^0.9.0",
"qrcode": "1.5.3",
"re-resizable": "^6.9.0",
@ -142,9 +141,8 @@
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
"@babel/traverse": "^7.12.12",
"@casualbot/jest-sonar-reporter": "2.2.7",
"@matrix-org/olm": "3.2.15",
"@knapsack-pro/cypress": "^8.0.1",
"@peculiar/webcrypto": "^1.4.3",
"@percy/cli": "^1.11.0",
"@percy/cypress": "^3.1.2",
@ -160,9 +158,8 @@
"@types/escape-html": "^1.0.1",
"@types/file-saver": "^2.0.3",
"@types/fs-extra": "^11.0.0",
"@types/geojson": "^7946.0.8",
"@types/glob-to-regexp": "^0.4.1",
"@types/jest": "29.5.6",
"@types/jest": "29.5.8",
"@types/katex": "^0.16.0",
"@types/lodash": "^4.14.168",
"@types/modernizr": "^3.5.3",
@ -175,7 +172,7 @@
"@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "17.0.21",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "2.9.3",
"@types/sanitize-html": "2.9.4",
"@types/sdp-transform": "^2.4.6",
"@types/tar-js": "^0.3.2",
"@types/ua-parser-js": "^0.7.36",
@ -186,14 +183,13 @@
"axe-core": "4.8.2",
"babel-jest": "^29.0.0",
"blob-polyfill": "^7.0.0",
"chokidar": "^3.5.1",
"cypress": "^12.0.0",
"cypress-axe": "^1.0.0",
"cypress-multi-reporters": "^1.6.1",
"cypress-plugin-init": "^0.0.8",
"cypress-real-events": "^1.7.1",
"cypress-terminal-report": "^5.3.2",
"eslint": "8.52.0",
"eslint": "8.53.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-deprecate": "0.8.4",
@ -214,7 +210,6 @@
"jest-raw-loader": "^1.0.1",
"jsqr": "^1.4.0",
"mailhog": "^4.16.0",
"matrix-mock-request": "^2.5.0",
"matrix-web-i18n": "^3.1.5",
"mocha-junit-reporter": "^2.2.0",
"node-fetch": "2",
@ -226,8 +221,7 @@
"stylelint-config-standard": "^34.0.0",
"stylelint-scss": "^5.0.0",
"ts-node": "^10.9.1",
"typescript": "5.1.6",
"walk": "^2.3.14"
"typescript": "5.1.6"
},
"peerDependencies": {
"postcss": "^8.4.19",

View file

@ -25,6 +25,7 @@ limitations under the License.
box-sizing: border-box;
height: 100%;
contain: strict;
background-color: var(--cpd-color-bg-canvas-default);
.mx_RoomView_MessageList {
padding: 14px 18px; /* top and bottom is 4px smaller to balance with the padding set above */

View file

@ -51,7 +51,6 @@ limitations under the License.
display: flex;
align-items: center;
justify-content: space-between;
width: calc(100% - 38px);
height: 24px;
flex: 1;
@ -157,12 +156,20 @@ limitations under the License.
.mx_BaseCard_back,
.mx_BaseCard_close {
flex-shrink: 0;
position: relative;
// @TODO(kerrya) background colours here are not semantic
// these buttons to be replaced with IconButton after secondary variant is added
// https://github.com/vector-im/compound/issues/279
background-color: var(--cpd-color-bg-subtle-secondary);
width: var(--BaseCard_header-button-size);
height: var(--BaseCard_header-button-size);
border-radius: 50%;
&:hover {
background-color: var(--cpd-color-bg-subtle-primary);
}
&::before {
content: "";
position: absolute;
@ -226,3 +233,7 @@ limitations under the License.
}
}
}
.mx_BaseCard_headerProp {
flex: 1 1 100%;
}

View file

@ -17,7 +17,7 @@ limitations under the License.
.mx_RoomSummaryCard {
.mx_RoomSummaryCard_container {
text-align: center;
margin-top: $spacing-20;
margin: $spacing-20 var(--cpd-space-4x) 0;
}
.mx_RoomSummaryCard_roomName,
@ -29,10 +29,6 @@ limitations under the License.
overflow: hidden;
}
.mx_RoomSummaryCard_roomName {
margin: $spacing-12 0 $spacing-4;
}
.mx_RoomSummaryCard_alias {
text-overflow: ellipsis;
}
@ -194,10 +190,6 @@ limitations under the License.
.mx_RoomSummaryCard_header {
padding: 15px 12px;
.mx_BaseCard_close {
flex-shrink: 0;
}
}
.mx_RoomSummaryCard_search input {
@ -207,42 +199,6 @@ limitations under the License.
cursor: pointer;
}
.mx_RoomSummaryCard_icon_people::before {
mask-image: url("$(res)/img/element-icons/room/members.svg");
}
.mx_RoomSummaryCard_icon_files::before {
mask-image: url("$(res)/img/element-icons/room/files.svg");
}
.mx_RoomSummaryCard_icon_pins::before {
mask-image: url("$(res)/img/element-icons/room/pin-upright.svg");
}
.mx_RoomSummaryCard_icon_threads::before {
mask-image: url("$(res)/img/element-icons/message/thread.svg");
}
.mx_RoomSummaryCard_icon_share::before {
mask-image: url("$(res)/img/element-icons/room/share.svg");
}
.mx_RoomSummaryCard_icon_settings::before {
mask-image: url("$(res)/img/element-icons/settings.svg");
}
.mx_RoomSummaryCard_icon_export::before {
mask-image: url("$(res)/img/element-icons/export.svg");
}
.mx_RoomSummaryCard_icon_poll::before {
mask-image: url("$(res)/img/element-icons/room/composer/poll.svg");
}
.mx_RoomSummaryCard_icon_search::before {
mask-image: url("$(res)/img/element-icons/room/search-inset.svg");
}
.mx_RoomSummaryCard_searchBtn {
background: var(--cpd-color-bg-canvas-default);
color: var(--cpd-color-icon-primary);
@ -252,9 +208,12 @@ limitations under the License.
height: 36px;
padding: var(--cpd-space-2x);
cursor: pointer;
transition: all 0.3s ease;
&:hover {
background: var(--cpd-color-bg-subtle-primary);
}
}
.mx_RoomSummaryCard_roomName {
margin: $spacing-12 0 $spacing-4;
}

View file

@ -55,6 +55,11 @@ limitations under the License.
height: 0;
opacity: 0;
transition: all var(--transition-standard) ease 0.1s;
/* Emojis are rendered a bit bigger than text in the timeline
Make them compact/the same size as text here */
.mx_Emoji {
font-size: inherit;
}
}
.mx_RoomHeader:hover .mx_RoomHeader_topic {
@ -83,8 +88,19 @@ limitations under the License.
cursor: pointer;
user-select: none;
/* RoomAvatar doesn't pass classes down to avatar
So set style here
using div because compound classes are not stable */
> div {
flex-shrink: 0;
}
&:hover {
color: $primary-content;
background: var(--cpd-color-bg-subtle-primary);
}
}
.mx_RoomHeader .mx_BaseAvatar {
flex-shrink: 0;
}

View file

@ -8,6 +8,6 @@
<g>
<path class="st0" d="M6,1.5L1.2,9.3c-0.2,0.3-0.2,0.8,0,1.1c0.2,0.3,0.6,0.6,1,0.6h9.7c0.4,0,0.8-0.2,1-0.6c0.2-0.3,0.2-0.8,0-1.1
L8,1.5C7.8,1.2,7.4,1,7,1C6.6,1,6.2,1.2,6,1.5z M7,4v3"/>
<path class="st0" d="M7,9L7,9.001"/>
</g>
<line class="st0" x1="7" y1="9" x2="7" y2="9"/>
</svg>

Before

Width:  |  Height:  |  Size: 709 B

After

Width:  |  Height:  |  Size: 700 B

View file

@ -136,6 +136,9 @@ export class PosthogAnalytics {
private authenticationType: Signup["authenticationType"] = "Other";
private watchSettingRef?: string;
// Will be set when the matrixClient is passed to the analytics object (e.g. on login).
private currentCryptoBackend?: "Rust" | "Legacy" = undefined;
public static get instance(): PosthogAnalytics {
if (!this._instance) {
this._instance = new PosthogAnalytics(posthog);
@ -170,6 +173,7 @@ export class PosthogAnalytics {
SettingsStore.monitorSetting("layout", null);
SettingsStore.monitorSetting("useCompactLayout", null);
this.onLayoutUpdated();
this.updateCryptoSuperProperty();
}
private onLayoutUpdated = (): void => {
@ -278,6 +282,8 @@ export class PosthogAnalytics {
this.registerSuperProperties(this.platformSuperProperties);
}
this.anonymity = anonymity;
// update anyhow, no-op if not enabled or Disabled.
this.updateCryptoSuperProperty();
}
private static getRandomAnalyticsId(): string {
@ -367,7 +373,28 @@ export class PosthogAnalytics {
this.registerSuperProperties(this.platformSuperProperties);
}
private updateCryptoSuperProperty(): void {
if (!this.enabled || this.anonymity === Anonymity.Disabled) return;
// Update super property for cryptoSDK in posthog.
// This property will be subsequently passed in every event.
if (this.currentCryptoBackend) {
this.registerSuperProperties({ cryptoSDK: this.currentCryptoBackend });
}
}
public async updateAnonymityFromSettings(client: MatrixClient, pseudonymousOptIn: boolean): Promise<void> {
// Temporary until we have migration code to switch crypto sdk.
if (client.getCrypto()) {
const cryptoVersion = client.getCrypto()!.getVersion();
// version for rust is something like "Rust SDK 0.6.0 (9c6b550), Vodozemac 0.5.0"
// for legacy it will be 'Olm x.x.x"
if (cryptoVersion.includes("Rust SDK")) {
this.currentCryptoBackend = "Rust";
} else {
this.currentCryptoBackend = "Legacy";
}
}
// Update this.anonymity based on the user's analytics opt-in settings
const anonymity = pseudonymousOptIn ? Anonymity.Pseudonymous : Anonymity.Disabled;
this.setAnonymity(anonymity);
@ -379,7 +406,8 @@ export class PosthogAnalytics {
}
if (anonymity !== Anonymity.Disabled) {
await PosthogAnalytics.instance.updatePlatformSuperProperties();
await this.updatePlatformSuperProperties();
this.updateCryptoSuperProperty();
}
}

View file

@ -16,7 +16,6 @@ limitations under the License.
import React, { useContext } from "react";
import { Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import { IProps as IContextMenuProps } from "../../structures/ContextMenu";
import IconizedContextMenu, {
@ -30,7 +29,6 @@ import { ButtonEvent } from "../elements/AccessibleButton";
import { DefaultTagID, TagID } from "../../../stores/room-list/models";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import dis from "../../../dispatcher/dispatcher";
import RoomListActions from "../../../actions/RoomListActions";
import { EchoChamber } from "../../../stores/local-echo/EchoChamber";
import { RoomNotifState } from "../../../RoomNotifs";
import Modal from "../../../Modal";
@ -52,6 +50,7 @@ import { SdkContextClass } from "../../../contexts/SDKContext";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { UIComponent } from "../../../settings/UIFeature";
import { DeveloperToolsOption } from "./DeveloperToolsOption";
import { tagRoom } from "../../../utils/room/tagRoom";
interface IProps extends IContextMenuProps {
room: Room;
@ -333,15 +332,7 @@ const RoomContextMenu: React.FC<IProps> = ({ room, onFinished, ...props }) => {
ev.preventDefault();
ev.stopPropagation();
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
dis.dispatch(RoomListActions.tagRoom(cli, room, removeTag, addTag, 0));
} else {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
}
tagRoom(room, tagId);
const action = getKeyBindingsManager().getAccessibilityAction(ev as React.KeyboardEvent);
switch (action) {

View file

@ -90,7 +90,7 @@ const BaseCard: React.FC<IProps> = forwardRef<HTMLDivElement, IProps>(
<div className="mx_BaseCard_header">
{backButton}
{closeButton}
{header}
<div className="mx_BaseCard_headerProp">{header}</div>
</div>
)}
{children}

View file

@ -16,25 +16,35 @@ limitations under the License.
import React, { useCallback, useContext, useEffect, useMemo, useState } from "react";
import classNames from "classnames";
import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix";
import { Badge, Heading, Text, Tooltip } from "@vector-im/compound-web";
import { MenuItem, Tooltip, Separator, ToggleMenuItem, Text, Badge, Heading } from "@vector-im/compound-web";
import { Icon as SearchIcon } from "@vector-im/compound-design-tokens/icons/search.svg";
import { Icon as FavouriteIcon } from "@vector-im/compound-design-tokens/icons/favourite-off.svg";
import { Icon as UserAddIcon } from "@vector-im/compound-design-tokens/icons/user-add.svg";
import { Icon as UserProfileSolidIcon } from "@vector-im/compound-design-tokens/icons/user-profile-solid.svg";
import { Icon as LinkIcon } from "@vector-im/compound-design-tokens/icons/link.svg";
import { Icon as SettingsIcon } from "@vector-im/compound-design-tokens/icons/settings.svg";
import { Icon as ExportArchiveIcon } from "@vector-im/compound-design-tokens/icons/export-archive.svg";
import { Icon as LeaveIcon } from "@vector-im/compound-design-tokens/icons/leave.svg";
import { Icon as FilesIcon } from "@vector-im/compound-design-tokens/icons/files.svg";
import { Icon as PollsIcon } from "@vector-im/compound-design-tokens/icons/polls.svg";
import { Icon as PinIcon } from "@vector-im/compound-design-tokens/icons/pin-off.svg";
import { Icon as LockIcon } from "@vector-im/compound-design-tokens/icons/lock.svg";
import { Icon as LockOffIcon } from "@vector-im/compound-design-tokens/icons/lock-off.svg";
import { Icon as PublicIcon } from "@vector-im/compound-design-tokens/icons/public.svg";
import { Icon as ErrorIcon } from "@vector-im/compound-design-tokens/icons/error.svg";
import { EventType, JoinRule, Room } from "matrix-js-sdk/src/matrix";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
import BaseCard, { Group } from "./BaseCard";
import { _t } from "../../../languageHandler";
import RoomAvatar from "../avatars/RoomAvatar";
import AccessibleButton, { ButtonEvent, IAccessibleButtonProps } from "../elements/AccessibleButton";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import Modal from "../../../Modal";
import ShareDialog from "../dialogs/ShareDialog";
import { useEventEmitter } from "../../../hooks/useEventEmitter";
import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter";
import WidgetUtils from "../../../utils/WidgetUtils";
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
import SettingsStore from "../../../settings/SettingsStore";
@ -47,7 +57,6 @@ import RoomContext from "../../../contexts/RoomContext";
import { UIComponent, UIFeature } from "../../../settings/UIFeature";
import { ChevronFace, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
import { WidgetContextMenu } from "../context_menus/WidgetContextMenu";
import { useRoomMemberCount } from "../../../hooks/useRoomMembers";
import { useFeatureEnabled } from "../../../hooks/useSettings";
import { usePinnedEvents } from "./PinnedMessagesCard";
import { Container, MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
@ -59,6 +68,11 @@ import PosthogTrackers from "../../../PosthogTrackers";
import { shouldShowComponent } from "../../../customisations/helpers/UIComponents";
import { PollHistoryDialog } from "../dialogs/PollHistoryDialog";
import { Flex } from "../../utils/Flex";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import { DefaultTagID } from "../../../stores/room-list/models";
import { tagRoom } from "../../../utils/room/tagRoom";
import { canInviteTo } from "../../../utils/room/canInviteTo";
import { inviteToRoom } from "../../../utils/room/inviteToRoom";
import { useAccountData } from "../../../hooks/useAccountData";
import { useRoomState } from "../../../hooks/useRoomState";
@ -73,23 +87,6 @@ interface IAppsSectionProps {
room: Room;
}
interface IButtonProps extends IAccessibleButtonProps {
className: string;
onClick(ev: ButtonEvent): void;
}
const Button: React.FC<IButtonProps> = ({ children, className, onClick, ...props }) => {
return (
<AccessibleButton
{...props}
className={classNames("mx_BaseCard_Button mx_RoomSummaryCard_Button", className)}
onClick={onClick}
>
{children}
</AccessibleButton>
);
};
export const useWidgets = (room: Room): IApp[] => {
const [apps, setApps] = useState<IApp[]>(() => WidgetStore.instance.getApps(room.roomId));
@ -263,11 +260,6 @@ const AppsSection: React.FC<IAppsSectionProps> = ({ room }) => {
);
};
const onRoomMembersClick = (ev: ButtonEvent): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
};
const onRoomFilesClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.FilePanel }, true);
};
@ -304,6 +296,18 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
});
};
const onLeaveRoomClick = (): void => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: room.roomId,
});
};
const onRoomMembersClick = (ev: ButtonEvent): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true);
PosthogTrackers.trackInteraction("WebRightPanelRoomInfoPeopleButton", ev);
};
const isRoomEncrypted = useIsEncrypted(cli, room);
const roomContext = useContext(RoomContext);
const e2eStatus = roomContext.e2eStatus;
@ -383,10 +387,14 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
</header>
);
const memberCount = useRoomMemberCount(room);
const pinningEnabled = useFeatureEnabled("feature_pinning");
const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length;
const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () =>
RoomListStore.instance.getTagsForRoom(room),
);
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
return (
<BaseCard header={null} className="mx_RoomSummaryCard" onClose={onClose}>
<Flex
@ -417,43 +425,58 @@ const RoomSummaryCard: React.FC<IProps> = ({ room, permalinkCreator, onClose, on
</Flex>
{header}
<Group title={_t("common|about")} className="mx_RoomSummaryCard_aboutGroup">
<Button className="mx_RoomSummaryCard_icon_people" onClick={onRoomMembersClick}>
{_t("common|people")}
<span className="mx_BaseCard_Button_sublabel">{memberCount}</span>
</Button>
{!isVideoRoom && (
<Button className="mx_RoomSummaryCard_icon_files" onClick={onRoomFilesClick}>
{_t("right_panel|files_button")}
</Button>
)}
{!isVideoRoom && (
<Button className="mx_RoomSummaryCard_icon_poll" onClick={onRoomPollHistoryClick}>
{_t("right_panel|polls_button")}
</Button>
)}
{pinningEnabled && !isVideoRoom && (
<Button className="mx_RoomSummaryCard_icon_pins" onClick={onRoomPinsClick}>
{_t("right_panel|pinned_messages_button")}
{pinCount > 0 && <span className="mx_BaseCard_Button_sublabel">{pinCount}</span>}
</Button>
)}
{!isVideoRoom && (
<Button className="mx_RoomSummaryCard_icon_export" onClick={onRoomExportClick}>
{_t("right_panel|export_chat_button")}
</Button>
)}
<Button
data-testid="shareRoomButton"
className="mx_RoomSummaryCard_icon_share"
onClick={onShareRoomClick}
>
{_t("right_panel|share_button")}
</Button>
<Button className="mx_RoomSummaryCard_icon_settings" onClick={onRoomSettingsClick}>
{_t("right_panel|settings_button")}
</Button>
</Group>
<Separator />
<ToggleMenuItem
Icon={FavouriteIcon}
label={_t("room|context_menu|favourite")}
checked={isFavorite}
onChange={() => tagRoom(room, DefaultTagID.Favourite)}
/>
<MenuItem
Icon={UserAddIcon}
label={_t("action|invite")}
disabled={!canInviteTo(room)}
onClick={() => inviteToRoom(room)}
/>
<MenuItem Icon={LinkIcon} label={_t("action|copy_link")} onClick={onShareRoomClick} />
<MenuItem Icon={SettingsIcon} label={_t("common|settings")} onClick={onRoomSettingsClick} />
<Separator />
<MenuItem
// this icon matches the legacy implementation
// and is a short term solution until legacy room header is removed
Icon={UserProfileSolidIcon}
label={_t("common|people")}
onClick={onRoomMembersClick}
/>
{!isVideoRoom && (
<>
<MenuItem Icon={FilesIcon} label={_t("right_panel|files_button")} onClick={onRoomFilesClick} />
<MenuItem
Icon={PollsIcon}
label={_t("right_panel|polls_button")}
onClick={onRoomPollHistoryClick}
/>
{pinningEnabled && (
<MenuItem
Icon={PinIcon}
label={_t("right_panel|pinned_messages_button")}
onClick={onRoomPinsClick}
>
<Text as="span" size="sm">
{pinCount}
</Text>
</MenuItem>
)}
<MenuItem Icon={ExportArchiveIcon} label={_t("export_chat|title")} onClick={onRoomExportClick} />
</>
)}
<Separator />
<MenuItem Icon={LeaveIcon} kind="critical" label={_t("action|leave_room")} onClick={onLeaveRoomClick} />
{SettingsStore.getValue(UIFeature.Widgets) &&
!isVideoRoom &&

View file

@ -16,7 +16,7 @@ limitations under the License.
import React, { useEffect, useMemo, useState } from "react";
import { Body as BodyText, IconButton, Tooltip } from "@vector-im/compound-web";
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call.svg";
import { Icon as VideoCallIcon } from "@vector-im/compound-design-tokens/icons/video-call-solid.svg";
import { Icon as VoiceCallIcon } from "@vector-im/compound-design-tokens/icons/voice-call.svg";
import { Icon as ThreadsIcon } from "@vector-im/compound-design-tokens/icons/threads-solid.svg";
import { Icon as NotificationsIcon } from "@vector-im/compound-design-tokens/icons/notifications-solid.svg";
@ -49,6 +49,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
import { formatCount } from "../../../utils/FormattingUtils";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { Linkify, topicToHtml } from "../../../HtmlUtils";
import PosthogTrackers from "../../../PosthogTrackers";
/**
* A helper to transform a notification color to the what the Compound Icon Button
@ -193,6 +194,15 @@ export default function RoomHeader({
</Tooltip>
);
})}
<Tooltip label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}>
<IconButton
disabled={!!videoCallDisabledReason}
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
onClick={videoCallClick}
>
<VideoCallIcon />
</IconButton>
</Tooltip>
{!useElementCallExclusively && (
<Tooltip label={!voiceCallDisabledReason ? _t("voip|voice_call") : voiceCallDisabledReason!}>
<IconButton
@ -204,21 +214,14 @@ export default function RoomHeader({
</IconButton>
</Tooltip>
)}
<Tooltip label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}>
<IconButton
disabled={!!videoCallDisabledReason}
aria-label={!videoCallDisabledReason ? _t("voip|video_call") : videoCallDisabledReason!}
onClick={videoCallClick}
>
<VideoCallIcon />
</IconButton>
</Tooltip>
<Tooltip label={_t("common|threads")}>
<IconButton
indicator={notificationColorToIndicator(threadNotifications)}
onClick={(evt) => {
evt.stopPropagation();
RightPanelStore.instance.showOrHidePanel(RightPanelPhases.ThreadPanel);
PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt);
}}
aria-label={_t("common|threads")}
>

View file

@ -418,7 +418,6 @@
"spaceinvaders_message": "sends space invaders"
},
"common": {
"about": "About",
"access_token": "Access Token",
"accessibility": "Accessibility",
"advanced": "Advanced",
@ -1854,8 +1853,6 @@
"room_summary_card": {
"title": "Room info"
},
"settings_button": "Room settings",
"share_button": "Share room",
"thread_list": {
"context_menu_label": "Thread options"
},

View file

@ -623,6 +623,7 @@ export class ElementCall extends Call {
public static readonly MEMBER_EVENT_TYPE = new NamespacedValue(null, EventType.GroupCallMemberPrefix);
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
private settingsStoreCallEncryptionWatcher: string | null = null;
private terminationTimer: number | null = null;
private _layout = Layout.Tile;
public get layout(): Layout {
@ -633,11 +634,7 @@ export class ElementCall extends Call {
this.emit(CallEvent.Layout, value);
}
private static createOrGetCallWidget(roomId: string, client: MatrixClient): IApp {
const ecWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.CALL.matches(app.type));
if (ecWidget) {
return ecWidget;
}
private static generateWidgetUrl(client: MatrixClient, roomId: string): URL {
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
// We really don't want the same analyticID's for the EC and EW posthog instances (Data on posthog should be limited/anonymized as much as possible).
@ -683,6 +680,20 @@ export class ElementCall extends Call {
const url = new URL(SdkConfig.get("element_call").url ?? DEFAULTS.element_call.url!);
url.pathname = "/room";
url.hash = `#?${params.toString()}`;
return url;
}
private static createOrGetCallWidget(roomId: string, client: MatrixClient): IApp {
const ecWidget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.CALL.matches(app.type));
const url = ElementCall.generateWidgetUrl(client, roomId);
if (ecWidget) {
// always update the url because even if the widget is already created
// we might have settings changes that update the widget.
ecWidget.url = url.toString();
return ecWidget;
}
// To use Element Call without touching room state, we create a virtual
// widget (one that doesn't have a corresponding state event)
@ -698,12 +709,21 @@ export class ElementCall extends Call {
roomId,
);
}
private onCallEncryptionSettingsChange(): void {
this.widget.url = ElementCall.generateWidgetUrl(this.client, this.roomId).toString();
}
private constructor(public session: MatrixRTCSession, widget: IApp, client: MatrixClient) {
super(widget, client);
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
SettingsStore.watchSetting(
"feature_disable_call_per_sender_encryption",
null,
this.onCallEncryptionSettingsChange.bind(this),
);
this.updateParticipants();
}
@ -784,6 +804,9 @@ export class ElementCall extends Call {
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
if (this.settingsStoreCallEncryptionWatcher) {
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
}
if (this.terminationTimer !== null) {
clearTimeout(this.terminationTimer);
this.terminationTimer = null;

View file

@ -0,0 +1,33 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { JoinRule, Room } from "matrix-js-sdk/src/matrix";
import { shouldShowComponent } from "../../customisations/helpers/UIComponents";
import { UIComponent } from "../../settings/UIFeature";
/**
* Can a user invite new members to the room
* @param room
* @returns whether the user can invite new members to the room
*/
export function canInviteTo(room: Room): boolean {
const client = room.client;
const canInvite =
!!room.canInvite(client.getSafeUserId()) || !!(room.isSpaceRoom() && room.getJoinRule() === JoinRule.Public);
return canInvite && room.getMyMembership() === "join" && shouldShowComponent(UIComponent.InviteUsers);
}

View file

@ -0,0 +1,36 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/matrix";
import dis from "../../dispatcher/dispatcher";
/**
* Invite to a room and prompts guests to registers
* @param room
*/
export function inviteToRoom(room: Room): void {
if (room.client.isGuest()) {
dis.dispatch({ action: "require_registration" });
return;
}
// open the room inviter
dis.dispatch({
action: "view_invite",
roomId: room.roomId,
});
}

40
src/utils/room/tagRoom.ts Normal file
View file

@ -0,0 +1,40 @@
/*
Copyright 2023 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Room } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
import RoomListStore from "../../stores/room-list/RoomListStore";
import { DefaultTagID, TagID } from "../../stores/room-list/models";
import RoomListActions from "../../actions/RoomListActions";
import dis from "../../dispatcher/dispatcher";
/**
* Toggle tag for a given room
* @param room The room to tag
* @param tagId The tag to invert
*/
export function tagRoom(room: Room, tagId: TagID): void {
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
const isApplied = RoomListStore.instance.getTagsForRoom(room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
dis.dispatch(RoomListActions.tagRoom(room.client, room, removeTag, addTag, 0));
} else {
logger.warn(`Unexpected tag ${tagId} applied to ${room.roomId}`);
}
}

View file

@ -16,6 +16,7 @@ limitations under the License.
import { mocked } from "jest-mock";
import { PostHog } from "posthog-js";
import { CryptoApi, MatrixClient } from "matrix-js-sdk/src/matrix";
import { Anonymity, getRedactedCurrentLocation, IPosthogEvent, PosthogAnalytics } from "../src/PosthogAnalytics";
import SdkConfig from "../src/SdkConfig";
@ -37,6 +38,7 @@ const getFakePosthog = (): PostHog =>
persistence: {
get_user_state: jest.fn(),
},
identifyUser: jest.fn(),
} as unknown as PostHog);
interface ITestEvent extends IPosthogEvent {
@ -274,4 +276,78 @@ describe("PosthogAnalytics", () => {
});
});
});
describe("CryptoSdk", () => {
let analytics: PosthogAnalytics;
const getFakeClient = (): MatrixClient =>
({
getCrypto: jest.fn(),
setAccountData: jest.fn(),
// just fake return an `im.vector.analytics` content
getAccountDataFromServer: jest.fn().mockReturnValue({
id: "0000000",
pseudonymousAnalyticsOptIn: true,
}),
} as unknown as MatrixClient);
beforeEach(async () => {
SdkConfig.put({
brand: "Testing",
posthog: {
project_api_key: "foo",
api_host: "bar",
},
});
analytics = new PosthogAnalytics(fakePosthog);
});
// `updateAnonymityFromSettings` is called On page load / login / account data change.
// We manually call it so we can test the behaviour.
async function simulateLogin(rustBackend: boolean, pseudonymous = true) {
// To simulate a switch we call updateAnonymityFromSettings.
// As per documentation this function is called On login.
const mockClient = getFakeClient();
mocked(mockClient.getCrypto).mockReturnValue({
getVersion: () => {
return rustBackend ? "Rust SDK 0.6.0 (9c6b550), Vodozemac 0.5.0" : "Olm 3.2.0";
},
} as unknown as CryptoApi);
await analytics.updateAnonymityFromSettings(mockClient, pseudonymous);
}
it("should send rust cryptoSDK superProperty correctly", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
await simulateLogin(false);
expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy");
});
it("should send Legacy cryptoSDK superProperty correctly", async () => {
analytics.setAnonymity(Anonymity.Pseudonymous);
await simulateLogin(false);
// Super Properties are properties associated with events that are set once and then sent with every capture call.
// They are set using posthog.register
expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Legacy");
});
it("should send cryptoSDK superProperty when enabling analytics", async () => {
analytics.setAnonymity(Anonymity.Disabled);
await simulateLogin(true, false);
// This initial call is due to the call to register platformSuperProperties
// The important thing is that the cryptoSDK superProperty is not set.
expect(mocked(fakePosthog).register.mock.lastCall![0]).toStrictEqual({});
// switching to pseudonymous should ensure that the cryptoSDK superProperty is set correctly
analytics.setAnonymity(Anonymity.Pseudonymous);
// Super Properties are properties associated with events that are set once and then sent with every capture call.
// They are set using posthog.register
expect(mocked(fakePosthog).register.mock.lastCall![0]["cryptoSDK"]).toStrictEqual("Rust");
});
});
});

View file

@ -16,7 +16,6 @@ limitations under the License.
import React from "react";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { jest } from "@jest/globals";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
@ -85,47 +84,6 @@ describe("RightPanel", () => {
const waitForRpsUpdate = () => new Promise<void>((resolve) => RightPanelStore.instance.once(UPDATE_EVENT, resolve));
it("navigates from room summary to member list", async () => {
const r1 = mkRoom(cli, "r1");
cli.getRoom.mockImplementation((roomId) => (roomId === "r1" ? r1 : null));
// Set up right panel state
const realGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((name, roomId) => {
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
if (roomId === "r1") {
return {
history: [{ phase: RightPanelPhases.RoomSummary }],
isOpen: true,
};
}
return null;
});
await spinUpStores();
const viewedRoom = waitForRpsUpdate();
dis.dispatch({
action: Action.ViewRoom,
room_id: "r1",
});
await viewedRoom;
const { container } = render(
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>,
);
expect(container.getElementsByClassName("mx_RoomSummaryCard")).toHaveLength(1);
const switchedPhases = waitForRpsUpdate();
userEvent.click(screen.getByText(/people/i));
await switchedPhases;
expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(1);
});
it("renders info from only one room during room changes", async () => {
const r1 = mkRoom(cli, "r1");
const r2 = mkRoom(cli, "r2");

View file

@ -20,22 +20,26 @@ exports[`AppTile destroys non-persisted right panel widget on room change 1`] =
title="Close"
/>
<div
class="mx_BaseCard_header_title"
class="mx_BaseCard_headerProp"
>
<h4
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
>
Example 1
</h4>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_BaseCard_header_title_button--option"
role="button"
tabindex="0"
title="Options"
/>
class="mx_BaseCard_header_title"
>
<h4
class="mx_Heading_h4 mx_BaseCard_header_title_heading"
>
Example 1
</h4>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="Options"
class="mx_AccessibleButton mx_BaseCard_header_title_button--option"
role="button"
tabindex="0"
title="Options"
/>
</div>
</div>
</div>
<div

View file

@ -33,6 +33,7 @@ import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } f
import { PollHistoryDialog } from "../../../../src/components/views/dialogs/PollHistoryDialog";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { _t } from "../../../../src/languageHandler";
import SettingsStore from "../../../../src/settings/SettingsStore";
describe("<RoomSummaryCard />", () => {
const userId = "@alice:domain.org";
@ -108,17 +109,6 @@ describe("<RoomSummaryCard />", () => {
expect(onSearchClick).toHaveBeenCalled();
});
it("opens room members list on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText("People"));
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith(
{ phase: RightPanelPhases.RoomMemberList },
true,
);
});
it("opens room file panel on button click", () => {
const { getByText } = getComponent();
@ -130,7 +120,7 @@ describe("<RoomSummaryCard />", () => {
it("opens room export dialog on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText("Export chat"));
fireEvent.click(getByText(_t("export_chat|title")));
expect(Modal.createDialog).toHaveBeenCalledWith(ExportDialog, { room });
});
@ -138,7 +128,7 @@ describe("<RoomSummaryCard />", () => {
it("opens share room dialog on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText("Share room"));
fireEvent.click(getByText(_t("action|copy_link")));
expect(Modal.createDialog).toHaveBeenCalledWith(ShareDialog, { target: room });
});
@ -146,11 +136,23 @@ describe("<RoomSummaryCard />", () => {
it("opens room settings on button click", () => {
const { getByText } = getComponent();
fireEvent.click(getByText("Room settings"));
fireEvent.click(getByText(_t("common|settings")));
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "open_room_settings" });
});
it("renders room members options when new room UI is not enabled", () => {
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
const { getByText } = getComponent();
fireEvent.click(getByText(_t("common|people")));
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith(
{ phase: RightPanelPhases.RoomMemberList },
true,
);
});
describe("pinning", () => {
it("renders pins options when pinning feature is enabled", () => {
mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning");

View file

@ -46,13 +46,13 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
!
</span>
<h1
class="_font-heading-md-semibold_1jx6b_121 mx_RoomSummaryCard_roomName text-primary"
class="_typography_yh5dq_162 _font-heading-md-semibold_yh5dq_121 mx_RoomSummaryCard_roomName text-primary"
title="!room:domain.org"
>
!room:domain.org
</h1>
<div
class="_font-body-sm-semibold_1jx6b_45 mx_RoomSummaryCard_alias text-secondary"
class="_typography_yh5dq_162 _font-body-sm-semibold_yh5dq_45 mx_RoomSummaryCard_alias text-secondary"
title=""
/>
<section
@ -60,7 +60,7 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: center; --mx-flex-gap: var(--cpd-space-2x);"
>
<span
class="_font-body-sm-medium_1jx6b_50 _badge_qipht_17"
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50 _badge_qipht_17"
data-kind="default"
>
<div
@ -71,60 +71,299 @@ exports[`<RoomSummaryCard /> renders the room summary 1`] = `
</section>
</header>
<div
class="mx_BaseCard_Group mx_RoomSummaryCard_aboutGroup"
class="_separator_1uqhh_17"
data-orientation="horizontal"
role="separator"
/>
<label
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
for=":r1:"
role="menuitemcheckbox"
>
<h2>
About
</h2>
<div
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_people"
role="button"
tabindex="0"
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Favourite
</span>
<div
class="_container_ik1u1_18"
>
<input
class="_input_ik1u1_32"
id=":r1:"
type="checkbox"
/>
<div
class="_ui_ik1u1_42"
/>
</div>
</label>
<button
class="_item_zxa40_17 _interactive_zxa40_36 _disabled_zxa40_125"
data-kind="primary"
disabled=""
role="menuitem"
>
<div
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Invite
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
role="menuitem"
>
<div
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Copy link
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
role="menuitem"
>
<div
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Settings
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<div
class="_separator_1uqhh_17"
data-orientation="horizontal"
role="separator"
/>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
role="menuitem"
>
<div
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
People
<span
class="mx_BaseCard_Button_sublabel"
>
0
</span>
</div>
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
role="menuitem"
>
<div
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_files"
role="button"
tabindex="0"
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Files
</div>
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
role="menuitem"
>
<div
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_poll"
role="button"
tabindex="0"
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Poll history
</div>
<div
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_export"
role="button"
tabindex="0"
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
Export chat
</div>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="primary"
role="menuitem"
>
<div
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_share"
data-testid="shareRoomButton"
role="button"
tabindex="0"
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Share room
</div>
Export Chat
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<div
class="_separator_1uqhh_17"
data-orientation="horizontal"
role="separator"
/>
<button
class="_item_zxa40_17 _interactive_zxa40_36"
data-kind="critical"
role="menuitem"
>
<div
class="mx_AccessibleButton mx_BaseCard_Button mx_RoomSummaryCard_Button mx_RoomSummaryCard_icon_settings"
role="button"
tabindex="0"
aria-hidden="true"
class="_icon_zxa40_49"
height="24"
width="24"
/>
<span
class="_typography_yh5dq_162 _font-body-md-medium_yh5dq_69 _label_zxa40_58"
>
Room settings
</div>
</div>
Leave room
</span>
<svg
aria-hidden="true"
class="_nav-hint_zxa40_65"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8.70005 17.3C8.51672 17.1167 8.42505 16.8834 8.42505 16.6C8.42505 16.3167 8.51672 16.0834 8.70005 15.9L12.6 12L8.70005 8.10005C8.51672 7.91672 8.42505 7.68338 8.42505 7.40005C8.42505 7.11672 8.51672 6.88338 8.70005 6.70005C8.88338 6.51672 9.11672 6.42505 9.40005 6.42505C9.68338 6.42505 9.91672 6.51672 10.1 6.70005L14.7 11.3C14.8 11.4 14.8709 11.5084 14.9125 11.625C14.9542 11.7417 14.975 11.8667 14.975 12C14.975 12.1334 14.9542 12.2584 14.9125 12.375C14.8709 12.4917 14.8 12.6 14.7 12.7L10.1 17.3C9.91672 17.4834 9.68338 17.575 9.40005 17.575C9.11672 17.575 8.88338 17.4834 8.70005 17.3Z"
fill="currentColor"
/>
</svg>
</button>
<div
class="mx_BaseCard_Group mx_RoomSummaryCard_appsGroup"
>

View file

@ -84,7 +84,11 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
tabindex="0"
title="Close"
/>
<span />
<div
class="mx_BaseCard_headerProp"
>
<span />
</div>
</div>
<div
class="mx_AutoHideScrollbar"

View file

@ -40,6 +40,7 @@ import {
resetAsyncStoreWithClient,
mockPlatformPeg,
mkEvent,
filterConsole,
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
@ -74,6 +75,10 @@ describe("LegacyRoomHeader", () => {
let bob: RoomMember;
let carol: RoomMember;
filterConsole(
"Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.",
);
beforeEach(async () => {
// some of our tests rely on the jest canvas mock, and `afterEach` will have reset the mock, so we need to
// restore it.

View file

@ -48,7 +48,10 @@ import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/Wid
jest.mock("../../../../src/utils/ShieldUtils");
describe("RoomHeader", () => {
filterConsole("[getType] Room !1:example.org does not have an m.room.create event");
filterConsole(
"[getType] Room !1:example.org does not have an m.room.create event",
"Age for event was not available, using `now - origin_server_ts` as a fallback. If the device clock is not correct issues might occur.",
);
let room: Room;

View file

@ -22,7 +22,7 @@ exports[`RoomHeader does not show the face pile for DMs 1`] = `
>
<div
aria-level="1"
class="_font-body-lg-semibold_1jx6b_83 mx_RoomHeader_heading"
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
dir="auto"
role="heading"
>

View file

@ -0,0 +1,105 @@
/*
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 { JoinRule, Room } from "matrix-js-sdk/src/matrix";
import { shouldShowComponent } from "../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../src/settings/UIFeature";
import { canInviteTo } from "../../../src/utils/room/canInviteTo";
import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../test-utils";
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
}));
describe("canInviteTo()", () => {
afterEach(() => {
jest.restoreAllMocks();
});
const userId = "@alice:server.org";
const roomId = "!room:server.org";
const makeRoom = (): Room => {
const client = getMockClientWithEventEmitter({
...mockClientMethodsUser(userId),
});
const room = new Room(roomId, client, userId);
jest.spyOn(room, "getMyMembership").mockReturnValue("join");
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
jest.spyOn(room, "canInvite").mockReturnValue(true);
return room;
};
beforeEach(() => {
mocked(shouldShowComponent).mockReturnValue(true);
});
describe("when user has permissions to issue an invite for this room", () => {
// aka when Room.canInvite is true
it("should return false when current user membership is not joined", () => {
const room = makeRoom();
jest.spyOn(room, "getMyMembership").mockReturnValue("invite");
expect(canInviteTo(room)).toEqual(false);
});
it("should return false when UIComponent.InviteUsers customisation hides invite", () => {
const room = makeRoom();
mocked(shouldShowComponent).mockReturnValue(false);
expect(canInviteTo(room)).toEqual(false);
expect(shouldShowComponent).toHaveBeenCalledWith(UIComponent.InviteUsers);
});
it("should return true when user can invite and is a room member", () => {
const room = makeRoom();
expect(canInviteTo(room)).toEqual(true);
});
});
describe("when user does not have permissions to issue an invite for this room", () => {
// aka when Room.canInvite is false
it("should return false when room is a private space", () => {
const room = makeRoom();
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
jest.spyOn(room, "canInvite").mockReturnValue(false);
expect(canInviteTo(room)).toEqual(false);
});
it("should return false when room is just a room", () => {
const room = makeRoom();
jest.spyOn(room, "canInvite").mockReturnValue(false);
expect(canInviteTo(room)).toEqual(false);
});
it("should return true when room is a public space", () => {
const room = makeRoom();
// default join rule is public
jest.spyOn(room, "isSpaceRoom").mockReturnValue(true);
jest.spyOn(room, "canInvite").mockReturnValue(false);
expect(canInviteTo(room)).toEqual(true);
});
});
});

View file

@ -0,0 +1,65 @@
/*
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 defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { inviteToRoom } from "../../../src/utils/room/inviteToRoom";
import { getMockClientWithEventEmitter } from "../../test-utils";
describe("inviteToRoom()", () => {
const userId = "@alice:server.org";
const roomId = "!room:server.org";
const makeRoom = (): Room => {
const client = getMockClientWithEventEmitter({
isGuest: jest.fn(),
});
const room = new Room(roomId, client, userId);
return room;
};
beforeEach(() => {
// stub
jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it("requires registration when a guest tries to invite to a room", () => {
const room = makeRoom();
jest.spyOn(room.client, "isGuest").mockReturnValue(true);
inviteToRoom(room);
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "require_registration" });
});
it("opens the room inviter", () => {
const room = makeRoom();
jest.spyOn(room.client, "isGuest").mockReturnValue(false);
inviteToRoom(room);
expect(defaultDispatcher.dispatch).toHaveBeenCalledTimes(1);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "view_invite", roomId });
});
});

View file

@ -0,0 +1,154 @@
/*
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 RoomListActions from "../../../src/actions/RoomListActions";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { DefaultTagID, TagID } from "../../../src/stores/room-list/models";
import RoomListStore from "../../../src/stores/room-list/RoomListStore";
import { tagRoom } from "../../../src/utils/room/tagRoom";
import { getMockClientWithEventEmitter } from "../../test-utils";
describe("tagRoom()", () => {
const userId = "@alice:server.org";
const roomId = "!room:server.org";
const makeRoom = (tags: TagID[] = []): Room => {
const client = getMockClientWithEventEmitter({
isGuest: jest.fn(),
});
const room = new Room(roomId, client, userId);
jest.spyOn(RoomListStore.instance, "getTagsForRoom").mockReturnValue(tags);
return room;
};
beforeEach(() => {
// stub
jest.spyOn(defaultDispatcher, "dispatch").mockImplementation(() => {});
jest.spyOn(RoomListActions, "tagRoom").mockReturnValue({ action: "mocked_tag_room_action", fn: () => {} });
});
afterEach(() => {
jest.restoreAllMocks();
});
it("does nothing when room tag is not allowed", () => {
const room = makeRoom();
tagRoom(room, DefaultTagID.ServerNotice);
expect(defaultDispatcher.dispatch).not.toHaveBeenCalled();
expect(RoomListActions.tagRoom).not.toHaveBeenCalled();
});
describe("when a room has no tags", () => {
it("should tag a room as favourite", () => {
const room = makeRoom();
tagRoom(room, DefaultTagID.Favourite);
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
room.client,
room,
DefaultTagID.LowPriority, // remove
DefaultTagID.Favourite, // add
0,
);
});
it("should tag a room low priority", () => {
const room = makeRoom();
tagRoom(room, DefaultTagID.LowPriority);
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
room.client,
room,
DefaultTagID.Favourite, // remove
DefaultTagID.LowPriority, // add
0,
);
});
});
describe("when a room is tagged as favourite", () => {
it("should unfavourite a room", () => {
const room = makeRoom([DefaultTagID.Favourite]);
tagRoom(room, DefaultTagID.Favourite);
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
room.client,
room,
DefaultTagID.Favourite, // remove
null, // add
0,
);
});
it("should tag a room low priority", () => {
const room = makeRoom([DefaultTagID.Favourite]);
tagRoom(room, DefaultTagID.LowPriority);
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
room.client,
room,
DefaultTagID.Favourite, // remove
DefaultTagID.LowPriority, // add
0,
);
});
});
describe("when a room is tagged as low priority", () => {
it("should favourite a room", () => {
const room = makeRoom([DefaultTagID.LowPriority]);
tagRoom(room, DefaultTagID.Favourite);
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
room.client,
room,
DefaultTagID.LowPriority, // remove
DefaultTagID.Favourite, // add
0,
);
});
it("should untag a room low priority", () => {
const room = makeRoom([DefaultTagID.LowPriority]);
tagRoom(room, DefaultTagID.LowPriority);
expect(defaultDispatcher.dispatch).toHaveBeenCalled();
expect(RoomListActions.tagRoom).toHaveBeenCalledWith(
room.client,
room,
DefaultTagID.LowPriority, // remove
null, // add
0,
);
});
});
});

1762
yarn.lock

File diff suppressed because it is too large Load diff