Merge branch 'develop' into renovate/typescript

This commit is contained in:
Michael Telatynski 2024-12-03 12:18:44 +00:00 committed by GitHub
commit a65a40eab3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
381 changed files with 4261 additions and 15995 deletions

View file

@ -39,7 +39,7 @@ jobs:
- name: Docker meta - name: Docker meta
id: meta id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5 uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5
with: with:
images: | images: |
vectorim/element-web vectorim/element-web
@ -51,7 +51,7 @@ jobs:
- name: Build and push - name: Build and push
id: build-and-push id: build-and-push
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6 uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6
with: with:
context: . context: .
push: true push: true

View file

@ -9,6 +9,6 @@ jobs:
action: action:
uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop uses: matrix-org/matrix-js-sdk/.github/workflows/pull_request.yaml@develop
permissions: permissions:
pull-requests: read pull-requests: write
secrets: secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }} ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}

View file

@ -19,8 +19,23 @@ on:
default: true default: true
permissions: {} # Uses ELEMENT_BOT_TOKEN instead permissions: {} # Uses ELEMENT_BOT_TOKEN instead
jobs: jobs:
checks:
name: Sanity checks
strategy:
matrix:
repo:
- matrix-org/matrix-js-sdk
- element-hq/element-web
- element-hq/element-desktop
uses: matrix-org/matrix-js-sdk/.github/workflows/release-checks.yml@develop
secrets:
ELEMENT_BOT_TOKEN: ${{ secrets.ELEMENT_BOT_TOKEN }}
with:
repository: ${{ matrix.repo }}
prepare: prepare:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: checks
env: env:
# The order is specified bottom-up to avoid any races for allchange # The order is specified bottom-up to avoid any races for allchange
REPOS: matrix-js-sdk element-web element-desktop REPOS: matrix-js-sdk element-web element-desktop

View file

@ -104,7 +104,7 @@ jobs:
- name: Skip SonarCloud in merge queue - name: Skip SonarCloud in merge queue
if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true' if: github.event_name == 'merge_group' || inputs.disable_coverage == 'true'
uses: guibranco/github-status-action-v2@1f26a0237cd1a57626fbb5a0eb2494c9b8797d07 uses: guibranco/github-status-action-v2@66088c44e212a906c32a047529a213d81809ec1c
with: with:
authToken: ${{ secrets.GITHUB_TOKEN }} authToken: ${{ secrets.GITHUB_TOKEN }}
state: success state: success

View file

@ -592,4 +592,3 @@ The following are undocumented or intended for developer use only.
2. `sync_timeline_limit` 2. `sync_timeline_limit`
3. `dangerously_allow_unsafe_and_insecure_passwords` 3. `dangerously_allow_unsafe_and_insecure_passwords`
4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled. 4. `latex_maths_delims`: An optional setting to override the default delimiters used for maths parsing. See https://github.com/matrix-org/matrix-react-sdk/pull/5939 for details. Only used when `feature_latex_maths` is enabled.
5. `voice_broadcast.chunk_length`: Target chunk length in seconds for the Voice Broadcast feature currently under development.

View file

@ -73,7 +73,7 @@
"resolutions": { "resolutions": {
"oidc-client-ts": "3.1.0", "oidc-client-ts": "3.1.0",
"jwt-decode": "4.0.0", "jwt-decode": "4.0.0",
"caniuse-lite": "1.0.30001679", "caniuse-lite": "1.0.30001684",
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0",
"wrap-ansi": "npm:wrap-ansi@^7.0.0" "wrap-ansi": "npm:wrap-ansi@^7.0.0"
}, },
@ -114,10 +114,10 @@
"jsrsasign": "^11.0.0", "jsrsasign": "^11.0.0",
"jszip": "^3.7.0", "jszip": "^3.7.0",
"katex": "^0.16.0", "katex": "^0.16.0",
"linkify-element": "4.1.3", "linkify-element": "4.1.4",
"linkify-react": "4.1.3", "linkify-react": "4.1.4",
"linkify-string": "4.1.3", "linkify-string": "4.1.4",
"linkifyjs": "4.1.3", "linkifyjs": "4.1.4",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"maplibre-gl": "^4.0.0", "maplibre-gl": "^4.0.0",
"matrix-encrypt-attachment": "^1.0.3", "matrix-encrypt-attachment": "^1.0.3",
@ -268,11 +268,12 @@
"postcss-preset-env": "^10.0.0", "postcss-preset-env": "^10.0.0",
"postcss-scss": "^4.0.4", "postcss-scss": "^4.0.4",
"postcss-simple-vars": "^7.0.1", "postcss-simple-vars": "^7.0.1",
"prettier": "3.3.3", "prettier": "3.4.1",
"process": "^0.11.10", "process": "^0.11.10",
"raw-loader": "^4.0.2", "raw-loader": "^4.0.2",
"rimraf": "^6.0.0", "rimraf": "^6.0.0",
"semver": "^7.5.2", "semver": "^7.5.2",
"source-map-loader": "^5.0.0",
"stylelint": "^16.1.0", "stylelint": "^16.1.0",
"stylelint-config-standard": "^36.0.0", "stylelint-config-standard": "^36.0.0",
"stylelint-scss": "^6.0.0", "stylelint-scss": "^6.0.0",

View file

@ -1,4 +1,4 @@
FROM mcr.microsoft.com/playwright:v1.48.2-jammy FROM mcr.microsoft.com/playwright:v1.49.0-jammy
WORKDIR /work WORKDIR /work

View file

@ -67,6 +67,9 @@ test.describe("Cryptography", function () {
await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click(); await page.locator(".mx_AuthPage").getByRole("button", { name: "I'll verify later" }).click();
await app.viewRoomByName("Test room"); await app.viewRoomByName("Test room");
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
await page.waitForTimeout(1000);
// There should be two historical events in the timeline // There should be two historical events in the timeline
const tiles = await page.locator(".mx_EventTile").all(); const tiles = await page.locator(".mx_EventTile").all();
expect(tiles.length).toBeGreaterThanOrEqual(2); expect(tiles.length).toBeGreaterThanOrEqual(2);

View file

@ -16,6 +16,7 @@ import {
logOutOfElement, logOutOfElement,
verify, verify,
} from "./utils"; } from "./utils";
import { bootstrapCrossSigningForClient } from "../../pages/client.ts";
test.describe("Cryptography", function () { test.describe("Cryptography", function () {
test.use({ test.use({
@ -307,5 +308,30 @@ test.describe("Cryptography", function () {
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
}); });
test("should show correct shields on events sent by users with changed identity", async ({
page,
app,
bot: bob,
homeserver,
}) => {
// Verify Bob
await verify(app, bob);
// Bob logs in a new device and resets cross-signing
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
/* should show an error for a message from a previously verified device */
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
const last = page.locator(".mx_EventTile_last");
await expect(last).toContainText("test encrypted from user that was previously verified");
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
await lastE2eIcon.focus();
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
"Sender's verified identity has changed",
);
});
}); });
}); });

View file

@ -0,0 +1,67 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
* Please see LICENSE files in the repository root for full details.
*/
import { test, expect } from "../../element-web-test";
test.describe("Share dialog", () => {
test.use({
displayName: "Alice",
room: async ({ app, user, bot }, use) => {
const roomId = await app.client.createRoom({ name: "Alice room" });
await use({ roomId });
},
});
test("should share a room", async ({ page, app, room }) => {
await app.viewRoomById(room.roomId);
await app.toggleRoomInfoPanel();
await page.getByRole("menuitem", { name: "Copy link" }).click();
const dialog = page.getByRole("dialog", { name: "Share room" });
await expect(dialog.getByText(`https://matrix.to/#/${room.roomId}`)).toBeVisible();
expect(dialog).toMatchScreenshot("share-dialog-room.png", {
// QRCode and url changes at every run
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
});
});
test("should share a room member", async ({ page, app, room, user }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
const rightPanel = await app.toggleRoomInfoPanel();
await rightPanel.getByRole("menuitem", { name: "People" }).click();
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
await rightPanel.getByRole("button", { name: "Share profile" }).click();
const dialog = page.getByRole("dialog", { name: "Share User" });
await expect(dialog.getByText(`https://matrix.to/#/${user.userId}`)).toBeVisible();
expect(dialog).toMatchScreenshot("share-dialog-user.png", {
// QRCode changes at every run
mask: [page.locator(".mx_QRCode")],
});
});
test("should share an event", async ({ page, app, room }) => {
await app.viewRoomById(room.roomId);
await app.client.sendMessage(room.roomId, { body: "hello", msgtype: "m.text" });
const timelineMessage = page.locator(".mx_MTextBody", { hasText: "hello" });
await timelineMessage.hover();
await page.getByRole("button", { name: "Options", exact: true }).click();
await page.getByRole("menuitem", { name: "Share" }).click();
const dialog = page.getByRole("dialog", { name: "Share Room Message" });
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
expect(dialog).toMatchScreenshot("share-dialog-event.png", {
// QRCode and url changes at every run
mask: [page.locator(".mx_QRCode"), page.locator(".mx_ShareDialog_top > span")],
});
await dialog.getByRole("checkbox", { name: "Link to selected message" }).click();
await expect(dialog.getByRole("checkbox", { name: "Link to selected message" })).not.toBeChecked();
});
});

View file

@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand";
// Docker tag to use for synapse docker image. // Docker tag to use for synapse docker image.
// We target a specific digest as every now and then a Synapse update will break our CI. // We target a specific digest as every now and then a Synapse update will break our CI.
// This digest is updated by the playwright-image-updates.yaml workflow periodically. // This digest is updated by the playwright-image-updates.yaml workflow periodically.
const DOCKER_TAG = "develop@sha256:e163b15bf4905e4067dece856cca00e6ac8d1d655f4f1307978eee256b3ea775"; const DOCKER_TAG = "develop@sha256:892793d00b70e9a92ceb929263fe734408ce7f50cb4436c65f07407048a6d4e7";
async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> { async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template); const templateDir = path.join(__dirname, "templates", opts.template);

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View file

@ -596,7 +596,7 @@ legend {
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
.mx_Dialog input[type="submit"], .mx_Dialog input[type="submit"],
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton), .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton),
.mx_Dialog_buttons input[type="submit"] { .mx_Dialog_buttons input[type="submit"] {
@ -616,14 +616,16 @@ legend {
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):last-child { ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(
.mx_ShareDialog button
):last-child {
margin-right: 0px; margin-right: 0px;
} }
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):focus, ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):focus,
.mx_Dialog input[type="submit"]:focus, .mx_Dialog input[type="submit"]:focus,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):focus,
.mx_Dialog_buttons input[type="submit"]:focus { .mx_Dialog_buttons input[type="submit"]:focus {
@ -635,7 +637,7 @@ legend {
.mx_Dialog_buttons .mx_Dialog_buttons
button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not( button.mx_Dialog_primary:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button), ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
.mx_Dialog_buttons input[type="submit"].mx_Dialog_primary { .mx_Dialog_buttons input[type="submit"].mx_Dialog_primary {
color: var(--cpd-color-text-on-solid-primary); color: var(--cpd-color-text-on-solid-primary);
background-color: var(--cpd-color-bg-action-primary-rest); background-color: var(--cpd-color-bg-action-primary-rest);
@ -648,7 +650,7 @@ legend {
.mx_Dialog_buttons .mx_Dialog_buttons
button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not( button.danger:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):not(.mx_UserProfileSettings button):not(
.mx_ThemeChoicePanel_CustomTheme button .mx_ThemeChoicePanel_CustomTheme button
):not(.mx_UnpinAllDialog button), ):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button),
.mx_Dialog_buttons input[type="submit"].danger { .mx_Dialog_buttons input[type="submit"].danger {
background-color: var(--cpd-color-bg-critical-primary); background-color: var(--cpd-color-bg-critical-primary);
border: solid 1px var(--cpd-color-bg-critical-primary); border: solid 1px var(--cpd-color-bg-critical-primary);
@ -664,7 +666,7 @@ legend {
.mx_Dialog .mx_Dialog
button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not( button:not(.mx_Dialog_nonDialogButton):not([class|="maplibregl"]):not(.mx_AccessibleButton):not(
.mx_UserProfileSettings button .mx_UserProfileSettings button
):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):disabled, ):not(.mx_ThemeChoicePanel_CustomTheme button):not(.mx_UnpinAllDialog button):not(.mx_ShareDialog button):disabled,
.mx_Dialog input[type="submit"]:disabled, .mx_Dialog input[type="submit"]:disabled,
.mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled, .mx_Dialog_buttons button:not(.mx_Dialog_nonDialogButton):not(.mx_AccessibleButton):disabled,
.mx_Dialog_buttons input[type="submit"]:disabled { .mx_Dialog_buttons input[type="submit"]:disabled {

View file

@ -393,9 +393,3 @@
@import "./views/voip/_LegacyCallViewHeader.pcss"; @import "./views/voip/_LegacyCallViewHeader.pcss";
@import "./views/voip/_LegacyCallViewSidebar.pcss"; @import "./views/voip/_LegacyCallViewSidebar.pcss";
@import "./views/voip/_VideoFeed.pcss"; @import "./views/voip/_VideoFeed.pcss";
@import "./voice-broadcast/atoms/_LiveBadge.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastControl.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastHeader.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastRecordingConnectionError.pcss";
@import "./voice-broadcast/atoms/_VoiceBroadcastRoomSubtitle.pcss";
@import "./voice-broadcast/molecules/_VoiceBroadcastBody.pcss";

View file

@ -22,20 +22,6 @@ Please see LICENSE files in the repository root for full details.
pointer-events: none; /* makes the avatar non-draggable */ pointer-events: none; /* makes the avatar non-draggable */
} }
} }
.mx_UserMenu_userAvatarLive {
align-items: center;
background-color: $alert;
border-radius: 6px;
color: $live-badge-color;
display: flex;
height: 12px;
justify-content: center;
left: 25px;
position: absolute;
top: 20px;
width: 12px;
}
} }
.mx_UserMenu_contextMenuButton { .mx_UserMenu_contextMenuButton {

View file

@ -5,50 +5,73 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
.mx_ShareDialog hr { .mx_ShareDialog {
margin-top: 25px; /* Value from figma design */
margin-bottom: 25px; width: 416px;
border-color: $light-fg-color;
.mx_Dialog_header {
text-align: center;
margin-bottom: var(--cpd-space-6x);
/* Override dialog header padding to able to center it */
padding-inline-end: 0;
} }
.mx_ShareDialog .mx_ShareDialog_content { .mx_ShareDialog_content {
margin: 10px 0; display: flex;
flex-direction: column;
gap: var(--cpd-space-6x);
align-items: center;
.mx_CopyableText { .mx_ShareDialog_top {
width: unset; /* full width */ display: flex;
flex-direction: column;
gap: var(--cpd-space-4x);
align-items: center;
width: 100%;
> a { span {
text-decoration: none; text-align: center;
flex-shrink: 1; font: var(--cpd-font-body-sm-semibold);
overflow: hidden; color: var(--cpd-color-text-secondary);
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} overflow: hidden;
width: 100%;
} }
} }
.mx_ShareDialog_split { label {
display: inline-flex;
gap: var(--cpd-space-3x);
justify-content: center;
align-items: center;
font: var(--cpd-font-body-md-medium);
}
button {
width: 100%;
}
.mx_ShareDialog_social {
display: flex; display: flex;
flex-wrap: wrap; gap: var(--cpd-space-3x);
} justify-content: center;
.mx_ShareDialog_qrcode_container { a {
float: left; width: 48px;
height: 256px; height: 48px;
width: 256px; border-radius: 99px;
margin-right: 64px; box-sizing: border-box;
} border: 1px solid var(--cpd-color-border-interactive-secondary);
display: flex;
justify-content: center;
align-items: center;
.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container { img {
width: 299px; width: 24px;
height: 24px;
}
}
} }
.mx_ShareDialog_social_container {
display: inline-block;
} }
.mx_ShareDialog_social_icon {
display: inline-grid;
margin-right: 10px;
margin-bottom: 10px;
} }

View file

@ -256,10 +256,6 @@ Please see LICENSE files in the repository root for full details.
mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg"); mask-image: url("@vector-im/compound-design-tokens/icons/mic-on-solid.svg");
} }
.mx_MessageComposer_voiceBroadcast::before {
mask-image: url("$(res)/img/element-icons/live.svg");
}
.mx_MessageComposer_plain_text::before { .mx_MessageComposer_plain_text::before {
mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg"); mask-image: url("$(res)/img/element-icons/room/composer/plain_text.svg");
} }

View file

@ -1,23 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_LiveBadge {
align-items: center;
background-color: $alert;
border-radius: 2px;
color: $live-badge-color;
display: inline-flex;
font-size: $font-12px;
font-weight: var(--cpd-font-weight-semibold);
gap: $spacing-4;
padding: 2px 4px;
}
.mx_LiveBadge--grey {
background-color: $quaternary-content;
}

View file

@ -1,28 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastControl {
align-items: center;
background-color: $background;
border-radius: 50%;
color: $secondary-content;
display: flex;
flex: 0 0 32px;
height: 32px;
justify-content: center;
width: 32px;
}
.mx_VoiceBroadcastControl-recording {
color: $alert;
}
.mx_VoiceBroadcastControl-play .mx_Icon {
left: 1px;
position: relative;
}

View file

@ -1,60 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastHeader {
align-items: flex-start;
display: flex;
gap: $spacing-8;
line-height: 20px;
margin-bottom: $spacing-16;
min-width: 0;
}
.mx_VoiceBroadcastHeader_content {
flex-grow: 1;
min-width: 0;
}
.mx_VoiceBroadcastHeader_room_wrapper {
align-items: center;
display: flex;
gap: 4px;
justify-content: flex-start;
}
.mx_VoiceBroadcastHeader_room {
font-size: $font-12px;
font-weight: var(--cpd-font-weight-semibold);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mx_VoiceBroadcastHeader_line {
align-items: center;
color: $secondary-content;
font-size: $font-12px;
display: flex;
gap: $spacing-4;
.mx_Spinner {
flex: 0 0 14px;
padding: 1px;
}
span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.mx_VoiceBroadcastHeader_mic--clickable {
cursor: pointer;
}

View file

@ -1,18 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastRecordingConnectionError {
align-items: center;
color: $alert;
display: flex;
gap: $spacing-12;
svg path {
fill: $alert;
}
}

View file

@ -1,14 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_RoomTile .mx_RoomTile_titleContainer .mx_RoomTile_subtitle.mx_RoomTile_subtitle--voice-broadcast {
align-items: center;
color: $alert;
display: flex;
gap: $spacing-4;
}

View file

@ -1,75 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
.mx_VoiceBroadcastBody {
background-color: $quinary-content;
border-radius: 8px;
color: $secondary-content;
display: inline-block;
font-size: $font-12px;
padding: $spacing-12;
width: 271px;
.mx_Clock {
line-height: 1;
}
}
.mx_VoiceBroadcastBody--pip {
background-color: $system;
box-shadow: 0 2px 8px 0 #0000004a;
}
.mx_VoiceBroadcastBody--small {
display: flex;
gap: $spacing-8;
width: 192px;
.mx_VoiceBroadcastHeader {
margin-bottom: 0;
}
.mx_VoiceBroadcastControl {
align-self: center;
}
.mx_LiveBadge {
margin-top: 4px;
}
}
.mx_VoiceBroadcastBody_divider {
background-color: $quinary-content;
border: 0;
height: 1px;
margin: $spacing-12 0;
}
.mx_VoiceBroadcastBody_controls {
align-items: center;
display: flex;
gap: $spacing-32;
justify-content: center;
margin-bottom: $spacing-8;
}
.mx_VoiceBroadcastBody_timerow {
display: flex;
justify-content: space-between;
}
.mx_AccessibleButton.mx_VoiceBroadcastBody_blockButton {
display: flex;
gap: $spacing-8;
}
.mx_VoiceBroadcastBody__small-close {
right: 8px;
position: absolute;
top: 8px;
}

View file

@ -240,11 +240,6 @@ $location-live-secondary-color: #deddfd;
} }
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
/* One-off colors */ /* One-off colors */
/* ******************** */ /* ******************** */
$progressbar-bg-color: var(--cpd-color-gray-200); $progressbar-bg-color: var(--cpd-color-gray-200);

View file

@ -226,11 +226,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: dark; color-scheme: dark;
} }

View file

@ -325,11 +325,6 @@ $location-live-color: #5c56f5;
$location-live-secondary-color: #deddfd; $location-live-secondary-color: #deddfd;
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: #ffffff;
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -10,8 +10,8 @@
/* Noto Color Emoji contains digits, in fixed-width, therefore causing /* Noto Color Emoji contains digits, in fixed-width, therefore causing
digits in flowed text to stand out. digits in flowed text to stand out.
TODO: Consider putting all emoji fonts to the end rather than the front. */ TODO: Consider putting all emoji fonts to the end rather than the front. */
$font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica", sans-serif, $font-family: "Inter", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Arial", "Helvetica",
"Noto Color Emoji"; sans-serif, "Noto Color Emoji";
$monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier", $monospace-font-family: "Inconsolata", var(--emoji-font-family), "Apple Color Emoji", "Segoe UI Emoji", "Courier",
monospace, "Noto Color Emoji"; monospace, "Noto Color Emoji";
@ -355,11 +355,6 @@ $location-live-color: var(--cpd-color-purple-900);
$location-live-secondary-color: var(--cpd-color-purple-600); $location-live-secondary-color: var(--cpd-color-purple-600);
/* ******************** */ /* ******************** */
/* Voice Broadcast */
/* ******************** */
$live-badge-color: var(--cpd-color-icon-on-solid-primary);
/* ******************** */
body { body {
color-scheme: light; color-scheme: light;
} }

View file

@ -1,47 +0,0 @@
#!/usr/bin/env python
import json
import sys
import os
if len(sys.argv) < 3:
print "Usage: %s <source> <dest>" % (sys.argv[0],)
print "eg. %s pt_BR.json pt.json" % (sys.argv[0],)
print
print "Adds any translations to <dest> that exist in <source> but not <dest>"
sys.exit(1)
srcpath = sys.argv[1]
dstpath = sys.argv[2]
tmppath = dstpath + ".tmp"
with open(srcpath) as f:
src = json.load(f)
with open(dstpath) as f:
dst = json.load(f)
toAdd = {}
for k,v in src.iteritems():
if k not in dst:
print "Adding %s" % (k,)
toAdd[k] = v
# don't just json.dumps as we'll probably re-order all the keys (and they're
# not in any given order so we can't just sort_keys). Append them to the end.
with open(dstpath) as ifp:
with open(tmppath, 'w') as ofp:
for line in ifp:
strippedline = line.strip()
if strippedline in ('{', '}'):
ofp.write(line)
elif strippedline.endswith(','):
ofp.write(line)
else:
ofp.write(' '+strippedline+',')
toAddStr = json.dumps(toAdd, indent=4, separators=(',', ': '), ensure_ascii=False, encoding="utf8").strip("{}\n")
ofp.write("\n")
ofp.write(toAddStr.encode('utf8'))
ofp.write("\n")
os.rename(tmppath, dstpath)

View file

@ -1,84 +0,0 @@
#!/usr/bin/env bash
# Fetches the js-sdk dependency for development or testing purposes
# If there exists a branch of that dependency with the same name as
# the branch the current checkout is on, use that branch. Otherwise,
# use develop.
set -x
GIT_CLONE_ARGS=("$@")
[ -z "$defbranch" ] && defbranch="develop"
# clone a specific branch of a github repo
function clone() {
org=$1
repo=$2
branch=$3
# Chop 'origin' off the start as jenkins ends up using
# branches on the origin, but this doesn't work if we
# specify the branch when cloning.
branch=${branch#origin/}
if [ -n "$branch" ]
then
echo "Trying to use $org/$repo#$branch"
# Disable auth prompts: https://serverfault.com/a/665959
GIT_TERMINAL_PROMPT=0 git clone https://github.com/$org/$repo.git $repo --branch $branch \
"${GIT_CLONE_ARGS[@]}"
return $?
fi
return 1
}
function dodep() {
deforg=$1
defrepo=$2
rm -rf $defrepo
# Try the PR author's branch in case it exists on the deps as well.
# Try the target branch of the push or PR.
# Use the default branch as the last resort.
if [[ "$BUILDKITE" == true ]]; then
# If BUILDKITE_BRANCH is set, it will contain either:
# * "branch" when the author's branch and target branch are in the same repo
# * "author:branch" when the author's branch is in their fork
# We can split on `:` into an array to check.
BUILDKITE_BRANCH_ARRAY=(${BUILDKITE_BRANCH//:/ })
if [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
prAuthor=${BUILDKITE_BRANCH_ARRAY[0]}
prBranch=${BUILDKITE_BRANCH_ARRAY[1]}
else
prAuthor=$deforg
prBranch=$BUILDKITE_BRANCH
fi
clone $prAuthor $defrepo $prBranch ||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH ||
clone $deforg $defrepo $defbranch ||
return $?
else
clone $deforg $defrepo $ghprbSourceBranch ||
clone $deforg $defrepo $GIT_BRANCH ||
clone $deforg $defrepo `git rev-parse --abbrev-ref HEAD` ||
clone $deforg $defrepo $defbranch ||
return $?
fi
echo "$defrepo set to branch "`git -C "$defrepo" rev-parse --abbrev-ref HEAD`
}
##############################
echo 'Setting up matrix-js-sdk'
dodep matrix-org matrix-js-sdk
pushd matrix-js-sdk
yarn link
yarn install --frozen-lockfile
popd
yarn link matrix-js-sdk
##############################

View file

@ -1,64 +0,0 @@
# Copyright 2017-2024 New Vector Ltd.
# SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
# Please see LICENSE in the repository root for full details.
# genflags.sh - Generates pngs for use with CountryDropdown.js
#
# Dependencies:
# - imagemagick --with-rsvg (because default imagemagick SVG
# renderer does not produce accurate results)
#
# on macOS, this is most easily done with:
# brew install imagemagick --with-librsvg
#
# This will clone the googlei18n flag repo before converting
# all phonenumber.js-supported country flags (as SVGs) into
# PNGs that can be used by CountryDropdown.js.
set -e
# Allow CTRL+C to terminate the script
trap "echo Exited!; exit;" SIGINT SIGTERM
# git clone the google repo to get flag SVGs
git clone git@github.com:googlei18n/region-flags
for f in region-flags/svg/*.svg; do
# Skip state flags
if [[ $f =~ [A-Z]{2}-[A-Z]{2,3}.svg ]] ; then
echo "Skipping state flag "$f
continue
fi
# Skip countries not included in phonenumber.js
if [[ $f =~ (AC|CP|DG|EA|EU|IC|TA|UM|UN|XK).svg ]] ; then
echo "Skipping non-phonenumber supported flag "$f
continue
fi
# Run imagemagick convert
# -background none : transparent background
# -resize 50x30 : resize the flag to have a height of 15px (2x)
# By default, aspect ratio is respected so the width will
# be correct and not necessarily 25px.
# -filter Lanczos : use sharper resampling to avoid muddiness
# -gravity Center : keep the image central when adding an -extent
# -border 1 : add a 1px border around the flag
# -bordercolor : set the border colour
# -extent 54x54 : surround the image with padding so that it
# has the dimensions 27x27px (2x).
convert $f -background none -filter Lanczos -resize 50x30 \
-gravity Center -border 1 -bordercolor \#e0e0e0 \
-extent 54x54 $f.png
# $f.png will be region-flags/svg/XX.svg.png at this point
# Extract filename from path $f
newname=${f##*/}
# Replace .svg with .png
newname=${newname%.svg}.png
# Move the file to flags directory
mv $f.png ../res/flags/$newname
echo "Generated res/flags/"$newname
done

View file

@ -10,7 +10,6 @@ import type { IWidget } from "matrix-widget-api";
import type { BLURHASH_FIELD } from "../utils/image-media"; import type { BLURHASH_FIELD } from "../utils/image-media";
import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types"; import type { JitsiCallMemberEventType, JitsiCallMemberContent } from "../call-types";
import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types"; import type { ILayoutStateEvent, WIDGET_LAYOUT_EVENT_TYPE } from "../stores/widgets/types";
import type { VoiceBroadcastInfoEventContent, VoiceBroadcastInfoEventType } from "../voice-broadcast/types";
import type { EncryptedFile } from "matrix-js-sdk/src/types"; import type { EncryptedFile } from "matrix-js-sdk/src/types";
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types // Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
@ -37,9 +36,6 @@ declare module "matrix-js-sdk/src/types" {
"im.vector.modular.widgets": IWidget | {}; "im.vector.modular.widgets": IWidget | {};
[WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent; [WIDGET_LAYOUT_EVENT_TYPE]: ILayoutStateEvent;
// Unstable voice broadcast state events
[VoiceBroadcastInfoEventType]: VoiceBroadcastInfoEventContent;
// Element custom state events // Element custom state events
"im.vector.web.settings": Record<string, any>; "im.vector.web.settings": Record<string, any>;
"org.matrix.room.preview_urls": { disable: boolean }; "org.matrix.room.preview_urls": { disable: boolean };
@ -78,7 +74,5 @@ declare module "matrix-js-sdk/src/types" {
waveform?: number[]; waveform?: number[];
}; };
"org.matrix.msc3245.voice"?: {}; "org.matrix.msc3245.voice"?: {};
"io.element.voice_broadcast_chunk"?: { sequence: number };
} }
} }

View file

@ -175,13 +175,6 @@ export interface IConfigOptions {
sync_timeline_limit?: number; sync_timeline_limit?: number;
dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option dangerously_allow_unsafe_and_insecure_passwords?: boolean; // developer option
voice_broadcast?: {
// length per voice chunk in seconds
chunk_length?: number;
// max voice broadcast length in seconds
max_length?: number;
};
user_notice?: { user_notice?: {
title: string; title: string;
description: string; description: string;

View file

@ -55,8 +55,6 @@ import { OpenInviteDialogPayload } from "./dispatcher/payloads/OpenInviteDialogP
import { findDMForUser } from "./utils/dm/findDMForUser"; import { findDMForUser } from "./utils/dm/findDMForUser";
import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers"; import { getJoinedNonFunctionalMembers } from "./utils/room/getJoinedNonFunctionalMembers";
import { localNotificationsAreSilenced } from "./utils/notifications"; import { localNotificationsAreSilenced } from "./utils/notifications";
import { SdkContextClass } from "./contexts/SDKContext";
import { showCantStartACallDialog } from "./voice-broadcast/utils/showCantStartACallDialog";
import { isNotNull } from "./Typeguards"; import { isNotNull } from "./Typeguards";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
import { Jitsi } from "./widgets/Jitsi.ts"; import { Jitsi } from "./widgets/Jitsi.ts";
@ -859,15 +857,6 @@ export default class LegacyCallHandler extends EventEmitter {
return; return;
} }
// Pause current broadcast, if any
SdkContextClass.instance.voiceBroadcastPlaybacksStore.getCurrent()?.pause();
if (SdkContextClass.instance.voiceBroadcastRecordingsStore.getCurrent()) {
// Do not start a call, if recording a broadcast
showCantStartACallDialog();
return;
}
// We might be using managed hybrid widgets // We might be using managed hybrid widgets
if (isManagedHybridWidgetEnabled(room)) { if (isManagedHybridWidgetEnabled(room)) {
await addManagedHybridWidget(room); await addManagedHybridWidget(room);

View file

@ -35,13 +35,11 @@ import IdentityAuthClient from "./IdentityAuthClient";
import { crossSigningCallbacks } from "./SecurityManager"; import { crossSigningCallbacks } from "./SecurityManager";
import { SlidingSyncManager } from "./SlidingSyncManager"; import { SlidingSyncManager } from "./SlidingSyncManager";
import { _t, UserFriendlyError } from "./languageHandler"; import { _t, UserFriendlyError } from "./languageHandler";
import { SettingLevel } from "./settings/SettingLevel";
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController"; import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
import ErrorDialog from "./components/views/dialogs/ErrorDialog"; import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import PlatformPeg from "./PlatformPeg"; import PlatformPeg from "./PlatformPeg";
import { formatList } from "./utils/FormattingUtils"; import { formatList } from "./utils/FormattingUtils";
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import { Features } from "./settings/Settings";
import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts"; import { setDeviceIsolationMode } from "./settings/controllers/DeviceIsolationModeController.ts";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
@ -333,11 +331,6 @@ class MatrixClientPegClass implements IMatrixClientPeg {
logger.error("Warning! Not using an encryption key for rust crypto store."); logger.error("Warning! Not using an encryption key for rust crypto store.");
} }
// Record the fact that we used the Rust crypto stack with this client. This just guards against people
// rolling back to versions of EW that did not default to Rust crypto (which would lead to an error, since
// we cannot migrate from Rust to Legacy crypto).
await SettingsStore.setValue(Features.RustCrypto, null, SettingLevel.DEVICE, true);
await this.matrixClient.initRustCrypto({ await this.matrixClient.initRustCrypto({
storageKey: rustCryptoStoreKey, storageKey: rustCryptoStoreKey,
storagePassword: rustCryptoStorePassword, storagePassword: rustCryptoStorePassword,

View file

@ -49,8 +49,6 @@ import { SdkContextClass } from "./contexts/SDKContext";
import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications"; import { localNotificationsAreSilenced, createLocalNotificationSettingsIfNeeded } from "./utils/notifications";
import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast"; import { getIncomingCallToastKey, IncomingCallToast } from "./toasts/IncomingCallToast";
import ToastStore from "./stores/ToastStore"; import ToastStore from "./stores/ToastStore";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName";
import { stripPlainReply } from "./utils/Reply"; import { stripPlainReply } from "./utils/Reply";
import { BackgroundAudio } from "./audio/BackgroundAudio"; import { BackgroundAudio } from "./audio/BackgroundAudio";
@ -81,17 +79,6 @@ const msgTypeHandlers: Record<string, (event: MatrixEvent) => string | null> = {
return TextForEvent.textForLocationEvent(event)(); return TextForEvent.textForLocationEvent(event)();
}, },
[MsgType.Audio]: (event: MatrixEvent): string | null => { [MsgType.Audio]: (event: MatrixEvent): string | null => {
if (event.getContent()?.[VoiceBroadcastChunkEventType]) {
if (event.getContent()?.[VoiceBroadcastChunkEventType]?.sequence === 1) {
// Show a notification for the first broadcast chunk.
// At this point a user received something to listen to.
return _t("notifier|io.element.voice_broadcast_chunk", { senderName: getSenderName(event) });
}
// Mute other broadcast chunks
return null;
}
return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet()); return TextForEvent.textForEvent(event, MatrixClientPeg.safeGet());
}, },
}; };
@ -460,8 +447,6 @@ class NotifierClass extends TypedEventEmitter<keyof EmittedEvents, EmittedEvents
// XXX: exported for tests // XXX: exported for tests
public evaluateEvent(ev: MatrixEvent): void { public evaluateEvent(ev: MatrixEvent): void {
// Mute notifications for broadcast info events
if (ev.getType() === VoiceBroadcastInfoEventType) return;
let roomId = ev.getRoomId()!; let roomId = ev.getRoomId()!;
if (LegacyCallHandler.instance.getSupportsVirtualRooms()) { if (LegacyCallHandler.instance.getSupportsVirtualRooms()) {
// Attempt to translate a virtual room to a native one // Attempt to translate a virtual room to a native one

View file

@ -46,10 +46,6 @@ export const DEFAULTS: DeepReadonly<IConfigOptions> = {
logo: require("../res/img/element-desktop-logo.svg").default, logo: require("../res/img/element-desktop-logo.svg").default,
url: "https://element.io/get-started", url: "https://element.io/get-started",
}, },
voice_broadcast: {
chunk_length: 2 * 60, // two minutes
max_length: 4 * 60 * 60, // four hours
},
feedback: { feedback: {
existing_issues_url: existing_issues_url:

View file

@ -36,7 +36,6 @@ import AccessibleButton from "./components/views/elements/AccessibleButton";
import RightPanelStore from "./stores/right-panel/RightPanelStore"; import RightPanelStore from "./stores/right-panel/RightPanelStore";
import { highlightEvent, isLocationEvent } from "./utils/EventUtils"; import { highlightEvent, isLocationEvent } from "./utils/EventUtils";
import { ElementCall } from "./models/Call"; import { ElementCall } from "./models/Call";
import { textForVoiceBroadcastStoppedEvent, VoiceBroadcastInfoEventType } from "./voice-broadcast";
import { getSenderName } from "./utils/event/getSenderName"; import { getSenderName } from "./utils/event/getSenderName";
import PosthogTrackers from "./PosthogTrackers.ts"; import PosthogTrackers from "./PosthogTrackers.ts";
@ -906,7 +905,6 @@ const stateHandlers: IHandlers = {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
"im.vector.modular.widgets": textForWidgetEvent, "im.vector.modular.widgets": textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent, [WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
[VoiceBroadcastInfoEventType]: textForVoiceBroadcastStoppedEvent,
}; };
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer

View file

@ -36,7 +36,7 @@ interface IState {
export default class EmbeddedPage extends React.PureComponent<IProps, IState> { export default class EmbeddedPage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false; private unmounted = false;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -34,6 +34,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -51,7 +52,7 @@ interface IState {
*/ */
class FilePanel extends React.Component<IProps, IState> { class FilePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// This is used to track if a decrypted event was a live event and should be // This is used to track if a decrypted event was a live event and should be
// added to the timeline. // added to the timeline.
@ -104,7 +105,11 @@ class FilePanel extends React.Component<IProps, IState> {
} }
if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) { if (!this.state.timelineSet.eventIdToTimeline(ev.getId()!)) {
this.state.timelineSet.addEventToTimeline(ev, timeline, false); this.state.timelineSet.addEventToTimeline(ev, timeline, {
fromCache: false,
addToState: false,
toStartOfTimeline: false,
});
} }
} }
@ -269,12 +274,10 @@ class FilePanel extends React.Component<IProps, IState> {
if (this.state.timelineSet) { if (this.state.timelineSet) {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.File}
timelineRenderingType: TimelineRenderingType.File, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
@ -298,16 +301,11 @@ class FilePanel extends React.Component<IProps, IState> {
layout={Layout.Group} layout={Layout.Group}
/> />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} else { } else {
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider {...this.context} timelineRenderingType={TimelineRenderingType.File}>
value={{
...this.context,
timelineRenderingType: TimelineRenderingType.File,
}}
>
<BaseCard <BaseCard
className="mx_FilePanel" className="mx_FilePanel"
onClose={this.props.onClose} onClose={this.props.onClose}
@ -315,7 +313,7 @@ class FilePanel extends React.Component<IProps, IState> {
> >
<Spinner /> <Spinner />
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -119,7 +119,6 @@ import { ValidatedServerConfig } from "../../utils/ValidatedServerConfig";
import { isLocalRoom } from "../../utils/localRoom/isLocalRoom"; import { isLocalRoom } from "../../utils/localRoom/isLocalRoom";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SDKContext, SdkContextClass } from "../../contexts/SDKContext";
import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings"; import { viewUserDeviceSettings } from "../../actions/handlers/viewUserDeviceSettings";
import { cleanUpBroadcasts, VoiceBroadcastResumer } from "../../voice-broadcast";
import GenericToast from "../views/toasts/GenericToast"; import GenericToast from "../views/toasts/GenericToast";
import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog"; import RovingSpotlightDialog from "../views/dialogs/spotlight/SpotlightDialog";
import { findDMForUser } from "../../utils/dm/findDMForUser"; import { findDMForUser } from "../../utils/dm/findDMForUser";
@ -227,7 +226,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private focusNext: FocusNextType; private focusNext: FocusNextType;
private subTitleStatus: string; private subTitleStatus: string;
private prevWindowWidth: number; private prevWindowWidth: number;
private voiceBroadcastResumer?: VoiceBroadcastResumer;
private readonly loggedInView = createRef<LoggedInViewType>(); private readonly loggedInView = createRef<LoggedInViewType>();
private dispatcherRef?: string; private dispatcherRef?: string;
@ -501,7 +499,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
window.removeEventListener("resize", this.onWindowResized); window.removeEventListener("resize", this.onWindowResized);
this.stores.accountPasswordStore.clearPassword(); this.stores.accountPasswordStore.clearPassword();
this.voiceBroadcastResumer?.destroy();
} }
private onWindowResized = (): void => { private onWindowResized = (): void => {
@ -651,10 +648,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
break; break;
case "logout": case "logout":
LegacyCallHandler.instance.hangupAllCalls(); LegacyCallHandler.instance.hangupAllCalls();
Promise.all([ Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
...[...CallStore.instance.connectedCalls].map((call) => call.disconnect()), Lifecycle.logout(this.stores.oidcClientStore),
cleanUpBroadcasts(this.stores), );
]).finally(() => Lifecycle.logout(this.stores.oidcClientStore));
break; break;
case "require_registration": case "require_registration":
startAnyRegistrationFlow(payload as any); startAnyRegistrationFlow(payload as any);
@ -1679,8 +1675,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
} }
}); });
this.voiceBroadcastResumer = new VoiceBroadcastResumer(cli);
} }
/** /**

View file

@ -196,7 +196,7 @@ interface IReadReceiptForUser {
*/ */
export default class MessagePanel extends React.Component<IProps, IState> { export default class MessagePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public static defaultProps = { public static defaultProps = {
disableGrouping: false, disableGrouping: false,

View file

@ -19,6 +19,7 @@ import { Layout } from "../../settings/enums/Layout";
import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import Measured from "../views/elements/Measured"; import Measured from "../views/elements/Measured";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
onClose(): void; onClose(): void;
@ -33,7 +34,7 @@ interface IState {
*/ */
export default class NotificationPanel extends React.PureComponent<IProps, IState> { export default class NotificationPanel extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private card = React.createRef<HTMLDivElement>(); private card = React.createRef<HTMLDivElement>();
@ -79,12 +80,10 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Notification}
timelineRenderingType: TimelineRenderingType.Notification, narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
header={_t("notifications|enable_prompt_toast_title")} header={_t("notifications|enable_prompt_toast_title")}
@ -99,7 +98,7 @@ export default class NotificationPanel extends React.PureComponent<IProps, IStat
{this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />} {this.card.current && <Measured sensor={this.card.current} onMeasurement={this.onMeasurement} />}
{content} {content}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { MutableRefObject, ReactNode, useContext, useRef } from "react"; import React, { MutableRefObject, ReactNode, useRef } from "react";
import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { CallEvent, CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { Optional } from "matrix-events-sdk"; import { Optional } from "matrix-events-sdk";
@ -21,19 +21,7 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore"; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import {
useCurrentVoiceBroadcastPreRecording,
useCurrentVoiceBroadcastRecording,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybackBody,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingPip,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
VoiceBroadcastSmallPlaybackBody,
} from "../../voice-broadcast";
import { useCurrentVoiceBroadcastPlayback } from "../../voice-broadcast/hooks/useCurrentVoiceBroadcastPlayback";
import { WidgetPip } from "../views/pips/WidgetPip"; import { WidgetPip } from "../views/pips/WidgetPip";
const SHOW_CALL_IN_STATES = [ const SHOW_CALL_IN_STATES = [
@ -46,9 +34,6 @@ const SHOW_CALL_IN_STATES = [
]; ];
interface IProps { interface IProps {
voiceBroadcastRecording: Optional<VoiceBroadcastRecording>;
voiceBroadcastPreRecording: Optional<VoiceBroadcastPreRecording>;
voiceBroadcastPlayback: Optional<VoiceBroadcastPlayback>;
movePersistedElement: MutableRefObject<(() => void) | undefined>; movePersistedElement: MutableRefObject<(() => void) | undefined>;
} }
@ -245,52 +230,9 @@ class PipContainerInner extends React.Component<IProps, IState> {
this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId }); this.setState({ showWidgetInPip, persistentWidgetId, persistentRoomId });
} }
private createVoiceBroadcastPlaybackPipContent(voiceBroadcastPlayback: VoiceBroadcastPlayback): CreatePipChildren {
const content =
this.state.viewedRoomId === voiceBroadcastPlayback.infoEvent.getRoomId() ? (
<VoiceBroadcastPlaybackBody playback={voiceBroadcastPlayback} pip={true} />
) : (
<VoiceBroadcastSmallPlaybackBody playback={voiceBroadcastPlayback} />
);
return ({ onStartMoving }) => (
<div key={`vb-playback-${voiceBroadcastPlayback.infoEvent.getId()}`} onMouseDown={onStartMoving}>
{content}
</div>
);
}
private createVoiceBroadcastPreRecordingPipContent(
voiceBroadcastPreRecording: VoiceBroadcastPreRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key="vb-pre-recording" onMouseDown={onStartMoving}>
<VoiceBroadcastPreRecordingPip voiceBroadcastPreRecording={voiceBroadcastPreRecording} />
</div>
);
}
private createVoiceBroadcastRecordingPipContent(
voiceBroadcastRecording: VoiceBroadcastRecording,
): CreatePipChildren {
return ({ onStartMoving }) => (
<div key={`vb-recording-${voiceBroadcastRecording.infoEvent.getId()}`} onMouseDown={onStartMoving}>
<VoiceBroadcastRecordingPip recording={voiceBroadcastRecording} />
</div>
);
}
public render(): ReactNode { public render(): ReactNode {
const pipMode = true; const pipMode = true;
let pipContent: Array<CreatePipChildren> = []; const pipContent: Array<CreatePipChildren> = [];
if (this.props.voiceBroadcastRecording) {
pipContent = [this.createVoiceBroadcastRecordingPipContent(this.props.voiceBroadcastRecording)];
} else if (this.props.voiceBroadcastPreRecording) {
pipContent = [this.createVoiceBroadcastPreRecordingPipContent(this.props.voiceBroadcastPreRecording)];
} else if (this.props.voiceBroadcastPlayback) {
pipContent = [this.createVoiceBroadcastPlaybackPipContent(this.props.voiceBroadcastPlayback)];
}
if (this.state.primaryCall) { if (this.state.primaryCall) {
// get a ref to call inside the current scope // get a ref to call inside the current scope
@ -338,24 +280,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
} }
export const PipContainer: React.FC = () => { export const PipContainer: React.FC = () => {
const sdkContext = useContext(SDKContext);
const voiceBroadcastPreRecordingStore = sdkContext.voiceBroadcastPreRecordingStore;
const { currentVoiceBroadcastPreRecording } = useCurrentVoiceBroadcastPreRecording(voiceBroadcastPreRecordingStore);
const voiceBroadcastRecordingsStore = sdkContext.voiceBroadcastRecordingsStore;
const { currentVoiceBroadcastRecording } = useCurrentVoiceBroadcastRecording(voiceBroadcastRecordingsStore);
const voiceBroadcastPlaybacksStore = sdkContext.voiceBroadcastPlaybacksStore;
const { currentVoiceBroadcastPlayback } = useCurrentVoiceBroadcastPlayback(voiceBroadcastPlaybacksStore);
const movePersistedElement = useRef<() => void>(); const movePersistedElement = useRef<() => void>();
return ( return <PipContainerInner movePersistedElement={movePersistedElement} />;
<PipContainerInner
voiceBroadcastPlayback={currentVoiceBroadcastPlayback}
voiceBroadcastPreRecording={currentVoiceBroadcastPreRecording}
voiceBroadcastRecording={currentVoiceBroadcastRecording}
movePersistedElement={movePersistedElement}
/>
);
}; };

View file

@ -63,7 +63,7 @@ interface IState {
export default class RightPanel extends React.Component<Props, IState> { export default class RightPanel extends React.Component<Props, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: Props, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -109,10 +109,10 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
// redraw the badge on the membership list // redraw the badge on the membership list
if (this.state.phase === RightPanelPhases.RoomMemberList) { if (this.state.phase === RightPanelPhases.MemberList) {
this.delayedUpdate(); this.delayedUpdate();
} else if ( } else if (
this.state.phase === RightPanelPhases.RoomMemberInfo && this.state.phase === RightPanelPhases.MemberInfo &&
member.userId === this.state.cardState?.member?.userId member.userId === this.state.cardState?.member?.userId
) { ) {
// refresh the member info (e.g. new power level) // refresh the member info (e.g. new power level)
@ -157,7 +157,7 @@ export default class RightPanel extends React.Component<Props, IState> {
const phase = this.props.overwriteCard?.phase ?? this.state.phase; const phase = this.props.overwriteCard?.phase ?? this.state.phase;
const cardState = this.props.overwriteCard?.state ?? this.state.cardState; const cardState = this.props.overwriteCard?.state ?? this.state.cardState;
switch (phase) { switch (phase) {
case RightPanelPhases.RoomMemberList: case RightPanelPhases.MemberList:
if (!!roomId) { if (!!roomId) {
card = ( card = (
<MemberList <MemberList
@ -170,22 +170,8 @@ export default class RightPanel extends React.Component<Props, IState> {
); );
} }
break; break;
case RightPanelPhases.SpaceMemberList:
if (!!cardState?.spaceId || !!roomId) {
card = (
<MemberList
roomId={cardState?.spaceId ?? roomId!}
key={cardState?.spaceId ?? roomId!}
onClose={this.onClose}
searchQuery={this.state.searchQuery}
onSearchQueryChanged={this.onSearchQueryChanged}
/>
);
}
break;
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.MemberInfo:
case RightPanelPhases.SpaceMemberInfo:
case RightPanelPhases.EncryptionPanel: { case RightPanelPhases.EncryptionPanel: {
if (!!cardState?.member) { if (!!cardState?.member) {
const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined; const roomMember = cardState.member instanceof RoomMember ? cardState.member : undefined;
@ -203,8 +189,7 @@ export default class RightPanel extends React.Component<Props, IState> {
} }
break; break;
} }
case RightPanelPhases.Room3pidMemberInfo: case RightPanelPhases.ThreePidMemberInfo:
case RightPanelPhases.Space3pidMemberInfo:
if (!!cardState?.memberInfoEvent) { if (!!cardState?.memberInfoEvent) {
card = ( card = (
<ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} /> <ThirdPartyMemberInfo event={cardState.memberInfoEvent} key={roomId} onClose={this.onClose} />

View file

@ -26,7 +26,7 @@ import ErrorDialog from "../views/dialogs/ErrorDialog";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
import RoomContext from "../../contexts/RoomContext"; import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
const DEBUG = false; const DEBUG = false;
let debuglog = function (msg: string): void {}; let debuglog = function (msg: string): void {};
@ -53,7 +53,7 @@ interface Props {
export const RoomSearchView = forwardRef<ScrollPanel, Props>( export const RoomSearchView = forwardRef<ScrollPanel, Props>(
({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => { ({ term, scope, promise, abortController, resizeNotifier, className, onUpdate, inProgress }: Props, ref) => {
const client = useContext(MatrixClientContext); const client = useContext(MatrixClientContext);
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("showHiddenEvents");
const [highlights, setHighlights] = useState<string[] | null>(null); const [highlights, setHighlights] = useState<string[] | null>(null);
const [results, setResults] = useState<ISearchResults | null>(null); const [results, setResults] = useState<ISearchResults | null>(null);
const aborted = useRef(false); const aborted = useRef(false);

View file

@ -89,7 +89,7 @@ interface IState {
export default class RoomStatusBar extends React.PureComponent<IProps, IState> { export default class RoomStatusBar extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, useContext } from "react"; import React, { ChangeEvent, ComponentProps, createRef, ReactElement, ReactNode, RefObject, JSX } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import {
IRecommendedVersion, IRecommendedVersion,
@ -29,6 +29,7 @@ import {
MatrixError, MatrixError,
ISearchResults, ISearchResults,
THREAD_RELATION_TYPE, THREAD_RELATION_TYPE,
MatrixClient,
} from "matrix-js-sdk/src/matrix"; } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types"; import { KnownMembership } from "matrix-js-sdk/src/types";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
@ -54,7 +55,7 @@ import WidgetEchoStore from "../../stores/WidgetEchoStore";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import RoomContext, { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext"; import { TimelineRenderingType, MainSplitContentType } from "../../contexts/RoomContext";
import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils"; import { E2EStatus, shieldStatusForRoom } from "../../utils/ShieldUtils";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { IMatrixClientCreds } from "../../MatrixClientPeg"; import { IMatrixClientCreds } from "../../MatrixClientPeg";
@ -126,6 +127,7 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers";
import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel";
import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext";
const DEBUG = false; const DEBUG = false;
const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000; const PREVENT_MULTIPLE_JITSI_WITHIN = 30_000;
@ -233,6 +235,11 @@ export interface IRoomState {
liveTimeline?: EventTimeline; liveTimeline?: EventTimeline;
narrow: boolean; narrow: boolean;
msc3946ProcessDynamicPredecessor: boolean; msc3946ProcessDynamicPredecessor: boolean;
/**
* Whether the room is encrypted or not.
* If null, we are still determining the encryption status.
*/
isRoomEncrypted: boolean | null;
canAskToJoin: boolean; canAskToJoin: boolean;
promptAskToJoin: boolean; promptAskToJoin: boolean;
@ -246,6 +253,7 @@ interface LocalRoomViewProps {
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
onFileDrop: (dataTransfer: DataTransfer) => Promise<void>; onFileDrop: (dataTransfer: DataTransfer) => Promise<void>;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -255,7 +263,7 @@ interface LocalRoomViewProps {
* @returns {ReactElement} * @returns {ReactElement}
*/ */
function LocalRoomView(props: LocalRoomViewProps): ReactElement { function LocalRoomView(props: LocalRoomViewProps): ReactElement {
const context = useContext(RoomContext); const context = useScopedRoomContext("room");
const room = context.room as LocalRoom; const room = context.room as LocalRoom;
const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0]; const encryptionEvent = props.localRoom.currentState.getStateEvents(EventType.RoomEncryption)[0];
let encryptionTile: ReactNode; let encryptionTile: ReactNode;
@ -323,6 +331,7 @@ interface ILocalRoomCreateLoaderProps {
localRoom: LocalRoom; localRoom: LocalRoom;
names: string; names: string;
resizeNotifier: ResizeNotifier; resizeNotifier: ResizeNotifier;
mainSplitContentType: MainSplitContentType;
} }
/** /**
@ -363,7 +372,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
private roomViewBody = createRef<HTMLDivElement>(); private roomViewBody = createRef<HTMLDivElement>();
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IRoomProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);
@ -417,6 +426,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
canAskToJoin: this.askToJoinEnabled, canAskToJoin: this.askToJoinEnabled,
promptAskToJoin: false, promptAskToJoin: false,
viewRoomOpts: { buttons: [] }, viewRoomOpts: { buttons: [] },
isRoomEncrypted: null,
}; };
} }
@ -655,6 +665,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
// the RoomView instance // the RoomView instance
if (initial) { if (initial) {
newState.room = this.context.client!.getRoom(newState.roomId) || undefined; newState.room = this.context.client!.getRoom(newState.roomId) || undefined;
newState.isRoomEncrypted = null;
if (newState.room) { if (newState.room) {
newState.showApps = this.shouldShowApps(newState.room); newState.showApps = this.shouldShowApps(newState.room);
this.onRoomLoaded(newState.room); this.onRoomLoaded(newState.room);
@ -697,6 +708,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (initial) { if (initial) {
this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek); this.setupRoom(newState.room, newState.roomId, !!newState.joining, !!newState.shouldPeek);
} }
// We don't block the initial setup but we want to make it early to not block the timeline rendering
const isRoomEncrypted = await this.getIsRoomEncrypted(newState.roomId);
this.setState({
isRoomEncrypted,
...(isRoomEncrypted &&
newState.roomId && { e2eStatus: RoomView.e2eStatusCache.get(newState.roomId) ?? E2EStatus.Warning }),
});
}; };
private onConnectedCalls = (): void => { private onConnectedCalls = (): void => {
@ -1214,18 +1233,18 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (payload.member) { if (payload.member) {
if (payload.push) { if (payload.push) {
RightPanelStore.instance.pushCard({ RightPanelStore.instance.pushCard({
phase: RightPanelPhases.RoomMemberInfo, phase: RightPanelPhases.MemberInfo,
state: { member: payload.member }, state: { member: payload.member },
}); });
} else { } else {
RightPanelStore.instance.setCards([ RightPanelStore.instance.setCards([
{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.RoomMemberList }, { phase: RightPanelPhases.MemberList },
{ phase: RightPanelPhases.RoomMemberInfo, state: { member: payload.member } }, { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } },
]); ]);
} }
} else { } else {
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomMemberList); RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList);
} }
break; break;
case Action.View3pidInvite: case Action.View3pidInvite:
@ -1342,13 +1361,12 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange); this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.onWidgetLayoutChange);
this.calculatePeekRules(room); this.calculatePeekRules(room);
this.updatePreviewUrlVisibility(room);
this.loadMembersIfJoined(room); this.loadMembersIfJoined(room);
this.calculateRecommendedVersion(room); this.calculateRecommendedVersion(room);
this.updateE2EStatus(room);
this.updatePermissions(room); this.updatePermissions(room);
this.checkWidgets(room); this.checkWidgets(room);
this.loadVirtualRoom(room); this.loadVirtualRoom(room);
this.updateRoomEncrypted(room);
if ( if (
this.getMainSplitContentType(room) !== MainSplitContentType.Timeline && this.getMainSplitContentType(room) !== MainSplitContentType.Timeline &&
@ -1377,6 +1395,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined; return room?.currentState.getStateEvents(EventType.RoomTombstone, "") ?? undefined;
} }
private async getIsRoomEncrypted(roomId = this.state.roomId): Promise<boolean> {
const crypto = this.context.client?.getCrypto();
if (!crypto || !roomId) return false;
return await crypto.isEncryptionEnabledInRoom(roomId);
}
private async calculateRecommendedVersion(room: Room): Promise<void> { private async calculateRecommendedVersion(room: Room): Promise<void> {
const upgradeRecommendation = await room.getRecommendedVersion(); const upgradeRecommendation = await room.getRecommendedVersion();
if (this.unmounted) return; if (this.unmounted) return;
@ -1409,12 +1434,15 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}); });
} }
private updatePreviewUrlVisibility({ roomId }: Room): void { private updatePreviewUrlVisibility(room: Room): void {
// URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit this.setState(({ isRoomEncrypted }) => ({
const key = this.context.client?.isRoomEncrypted(roomId) ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"; showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
this.setState({ }));
showUrlPreview: SettingsStore.getValue(key, roomId), }
});
private getPreviewUrlVisibility({ roomId }: Room, isRoomEncrypted: boolean | null): boolean {
const key = isRoomEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled";
return SettingsStore.getValue(key, roomId);
} }
private onRoom = (room: Room): void => { private onRoom = (room: Room): void => {
@ -1456,22 +1484,20 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
}; };
private async updateE2EStatus(room: Room): Promise<void> { private async updateE2EStatus(room: Room): Promise<void> {
if (!this.context.client?.isRoomEncrypted(room.roomId)) return; if (!this.context.client || !this.state.isRoomEncrypted) return;
const e2eStatus = await this.cacheAndGetE2EStatus(room, this.context.client);
// If crypto is not currently enabled, we aren't tracking devices at all,
// so we don't know what the answer is. Let's error on the safe side and show
// a warning for this case.
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId) ?? E2EStatus.Warning;
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
this.setState({ e2eStatus });
if (this.context.client.getCrypto()) {
/* At this point, the user has encryption on and cross-signing on */
e2eStatus = await shieldStatusForRoom(this.context.client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
if (this.unmounted) return; if (this.unmounted) return;
this.setState({ e2eStatus }); this.setState({ e2eStatus });
} }
private async cacheAndGetE2EStatus(room: Room, client: MatrixClient): Promise<E2EStatus> {
let e2eStatus = RoomView.e2eStatusCache.get(room.roomId);
// set the state immediately then update, so we don't scare the user into thinking the room is unencrypted
if (e2eStatus) this.setState({ e2eStatus });
e2eStatus = await shieldStatusForRoom(client, room);
RoomView.e2eStatusCache.set(room.roomId, e2eStatus);
return e2eStatus;
} }
private onUrlPreviewsEnabledChange = (): void => { private onUrlPreviewsEnabledChange = (): void => {
@ -1480,20 +1506,36 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
}; };
private onRoomStateEvents = (ev: MatrixEvent, state: RoomState): void => { private onRoomStateEvents = async (ev: MatrixEvent, state: RoomState): Promise<void> => {
// ignore if we don't have a room yet // ignore if we don't have a room yet
if (!this.state.room || this.state.room.roomId !== state.roomId) return; if (!this.state.room || this.state.room.roomId !== state.roomId || !this.context.client) return;
switch (ev.getType()) { switch (ev.getType()) {
case EventType.RoomTombstone: case EventType.RoomTombstone:
this.setState({ tombstone: this.getRoomTombstone() }); this.setState({ tombstone: this.getRoomTombstone() });
break; break;
case EventType.RoomEncryption: {
await this.updateRoomEncrypted();
break;
}
default: default:
this.updatePermissions(this.state.room); this.updatePermissions(this.state.room);
} }
}; };
private async updateRoomEncrypted(room = this.state.room): Promise<void> {
if (!room || !this.context.client) return;
const isRoomEncrypted = await this.getIsRoomEncrypted(room.roomId);
const newE2EStatus = isRoomEncrypted ? await this.cacheAndGetE2EStatus(room, this.context.client) : null;
this.setState({
isRoomEncrypted,
showUrlPreview: this.getPreviewUrlVisibility(room, isRoomEncrypted),
...(newE2EStatus && { e2eStatus: newE2EStatus }),
});
}
private onRoomStateUpdate = (state: RoomState): void => { private onRoomStateUpdate = (state: RoomState): void => {
// ignore members in other rooms // ignore members in other rooms
if (state.roomId !== this.state.room?.roomId) { if (state.roomId !== this.state.room?.roomId) {
@ -1959,35 +2001,41 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
if (!this.state.room || !this.context?.client) return null; if (!this.state.room || !this.context?.client) return null;
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId()); const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomCreateLoader localRoom={localRoom} names={names} resizeNotifier={this.props.resizeNotifier} /> <LocalRoomCreateLoader
</RoomContext.Provider> localRoom={localRoom}
names={names}
resizeNotifier={this.props.resizeNotifier}
mainSplitContentType={this.state.mainSplitContentType}
/>
</ScopedRoomContextProvider>
); );
} }
private renderLocalRoomView(localRoom: LocalRoom): ReactNode { private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<LocalRoomView <LocalRoomView
localRoom={localRoom} localRoom={localRoom}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
permalinkCreator={this.permalinkCreator} permalinkCreator={this.permalinkCreator}
roomView={this.roomView} roomView={this.roomView}
onFileDrop={this.onFileDrop} onFileDrop={this.onFileDrop}
mainSplitContentType={this.state.mainSplitContentType}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode { private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<WaitingForThirdPartyRoomView <WaitingForThirdPartyRoomView
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
roomView={this.roomView} roomView={this.roomView}
inviteEvent={inviteEvent} inviteEvent={inviteEvent}
/> />
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
@ -2027,6 +2075,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
public render(): ReactNode { public render(): ReactNode {
if (!this.context.client) return null; if (!this.context.client) return null;
const { isRoomEncrypted } = this.state;
const isRoomEncryptionLoading = isRoomEncrypted === null;
if (this.state.room instanceof LocalRoom) { if (this.state.room instanceof LocalRoom) {
if (this.state.room.state === LocalRoomState.CREATING) { if (this.state.room.state === LocalRoomState.CREATING) {
@ -2242,14 +2292,16 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let aux: JSX.Element | undefined; let aux: JSX.Element | undefined;
let previewBar; let previewBar;
if (this.state.timelineRenderingType === TimelineRenderingType.Search) { if (this.state.timelineRenderingType === TimelineRenderingType.Search) {
if (!isRoomEncryptionLoading) {
aux = ( aux = (
<RoomSearchAuxPanel <RoomSearchAuxPanel
searchInfo={this.state.search} searchInfo={this.state.search}
onCancelClick={this.onCancelSearchClick} onCancelClick={this.onCancelSearchClick}
onSearchScopeChange={this.onSearchScopeChange} onSearchScopeChange={this.onSearchScopeChange}
isRoomEncrypted={this.context.client.isRoomEncrypted(this.state.room.roomId)} isRoomEncrypted={isRoomEncrypted}
/> />
); );
}
} else if (showRoomUpgradeBar) { } else if (showRoomUpgradeBar) {
aux = <RoomUpgradeWarningBar room={this.state.room} />; aux = <RoomUpgradeWarningBar room={this.state.room} />;
} else if (myMembership !== KnownMembership.Join) { } else if (myMembership !== KnownMembership.Join) {
@ -2325,8 +2377,10 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
let messageComposer; let messageComposer;
const showComposer = const showComposer =
!isRoomEncryptionLoading &&
// joined and not showing search results // joined and not showing search results
myMembership === KnownMembership.Join && !this.state.search; myMembership === KnownMembership.Join &&
!this.state.search;
if (showComposer) { if (showComposer) {
messageComposer = ( messageComposer = (
<MessageComposer <MessageComposer
@ -2367,7 +2421,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
highlightedEventId = this.state.initialEventId; highlightedEventId = this.state.initialEventId;
} }
const messagePanel = ( let messagePanel: JSX.Element | undefined;
if (!isRoomEncryptionLoading) {
messagePanel = (
<TimelinePanel <TimelinePanel
ref={this.gatherTimelinePanelRef} ref={this.gatherTimelinePanelRef}
timelineSet={this.state.room.getUnfilteredTimelineSet()} timelineSet={this.state.room.getUnfilteredTimelineSet()}
@ -2395,6 +2451,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
editState={this.state.editState} editState={this.state.editState}
/> />
); );
}
let topUnreadMessagesBar: JSX.Element | undefined; let topUnreadMessagesBar: JSX.Element | undefined;
// Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense // Do not show TopUnreadMessagesBar if we have search results showing, it makes no sense
@ -2415,7 +2472,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
); );
} }
const showRightPanel = this.state.room && this.state.showRightPanel; const showRightPanel = !isRoomEncryptionLoading && this.state.room && this.state.showRightPanel;
const rightPanel = showRightPanel ? ( const rightPanel = showRightPanel ? (
<RightPanel <RightPanel
@ -2516,7 +2573,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
} }
return ( return (
<RoomContext.Provider value={this.state}> <ScopedRoomContextProvider {...this.state}>
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}> <div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
{showChatEffects && this.roomView.current && ( {showChatEffects && this.roomView.current && (
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} /> <EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
@ -2543,7 +2600,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
</MainSplit> </MainSplit>
</ErrorBoundary> </ErrorBoundary>
</div> </div>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -208,7 +208,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
const storeIsShowingSpaceMembers = useCallback( const storeIsShowingSpaceMembers = useCallback(
() => () =>
RightPanelStore.instance.isOpenForRoom(space.roomId) && RightPanelStore.instance.isOpenForRoom(space.roomId) &&
RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.SpaceMemberList, RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList,
[space.roomId], [space.roomId],
); );
const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers);
@ -251,7 +251,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => {
} }
const onMembersClick = (): void => { const onMembersClick = (): void => {
RightPanelStore.instance.setCard({ phase: RightPanelPhases.SpaceMemberList }); RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList });
}; };
return ( return (
@ -597,7 +597,7 @@ const SpaceSetupPrivateInvite: React.FC<{
export default class SpaceRoomView extends React.PureComponent<IProps, IState> { export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -20,7 +20,7 @@ import MatrixClientContext, { useMatrixClientContext } from "../../contexts/Matr
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton"; import { ContextMenuButton } from "../../accessibility/context_menu/ContextMenuButton";
import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu"; import ContextMenu, { ChevronFace, MenuItemRadio, useContextMenu } from "./ContextMenu";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../contexts/RoomContext";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import { Layout } from "../../settings/enums/Layout"; import { Layout } from "../../settings/enums/Layout";
import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../utils/permalinks/Permalinks";
@ -30,6 +30,7 @@ import { ButtonEvent } from "../views/elements/AccessibleButton";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import { clearRoomNotification } from "../../utils/notifications"; import { clearRoomNotification } from "../../utils/notifications";
import EmptyState from "../views/right_panel/EmptyState"; import EmptyState from "../views/right_panel/EmptyState";
import { ScopedRoomContextProvider, useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
roomId: string; roomId: string;
@ -68,7 +69,7 @@ export const ThreadPanelHeader: React.FC<{
setFilterOption: (filterOption: ThreadFilterType) => void; setFilterOption: (filterOption: ThreadFilterType) => void;
}> = ({ filterOption, setFilterOption }) => { }> = ({ filterOption, setFilterOption }) => {
const mxClient = useMatrixClientContext(); const mxClient = useMatrixClientContext();
const roomContext = useRoomContext(); const roomContext = useScopedRoomContext("room");
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>(); const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();
const options: readonly ThreadPanelHeaderOption[] = [ const options: readonly ThreadPanelHeaderOption[] = [
{ {
@ -184,13 +185,11 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
}, [timelineSet, timelinePanel]); }, [timelineSet, timelinePanel]);
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...roomContext}
...roomContext, timelineRenderingType={TimelineRenderingType.ThreadsList}
timelineRenderingType: TimelineRenderingType.ThreadsList, showHiddenEvents={true}
showHiddenEvents: true, narrow={narrow}
narrow,
}}
> >
<BaseCard <BaseCard
header={ header={
@ -241,7 +240,7 @@ const ThreadPanel: React.FC<IProps> = ({ roomId, onClose, permalinkCreator }) =>
</div> </div>
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
}; };
export default ThreadPanel; export default ThreadPanel;

View file

@ -51,6 +51,7 @@ import { ComposerInsertPayload, ComposerType } from "../../dispatcher/payloads/C
import Heading from "../views/typography/Heading"; import Heading from "../views/typography/Heading";
import { SdkContextClass } from "../../contexts/SDKContext"; import { SdkContextClass } from "../../contexts/SDKContext";
import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload"; import { ThreadPayload } from "../../dispatcher/payloads/ThreadPayload";
import { ScopedRoomContextProvider } from "../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
room: Room; room: Room;
@ -75,7 +76,7 @@ interface IState {
export default class ThreadView extends React.Component<IProps, IState> { export default class ThreadView extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private layoutWatcherRef?: string; private layoutWatcherRef?: string;
@ -422,14 +423,12 @@ export default class ThreadView extends React.Component<IProps, IState> {
} }
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={TimelineRenderingType.Thread}
timelineRenderingType: TimelineRenderingType.Thread, threadId={this.state.thread?.id}
threadId: this.state.thread?.id, liveTimeline={this.state?.thread?.timelineSet?.getLiveTimeline()}
liveTimeline: this.state?.thread?.timelineSet?.getLiveTimeline(), narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className={classNames("mx_ThreadView mx_ThreadPanel", { className={classNames("mx_ThreadView mx_ThreadPanel", {
@ -463,7 +462,7 @@ export default class ThreadView extends React.Component<IProps, IState> {
/> />
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -229,7 +229,7 @@ interface IEventIndexOpts {
*/ */
class TimelinePanel extends React.Component<IProps, IState> { class TimelinePanel extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
// a map from room id to read marker event timestamp // a map from room id to read marker event timestamp
public static roomReadMarkerTsMap: Record<string, number> = {}; public static roomReadMarkerTsMap: Record<string, number> = {};

View file

@ -40,8 +40,6 @@ import { UPDATE_SELECTED_SPACE } from "../../stores/spaces";
import UserIdentifierCustomisations from "../../customisations/UserIdentifier"; import UserIdentifierCustomisations from "../../customisations/UserIdentifier";
import PosthogTrackers from "../../PosthogTrackers"; import PosthogTrackers from "../../PosthogTrackers";
import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload";
import { Icon as LiveIcon } from "../../../res/img/compound/live-8px.svg";
import { VoiceBroadcastRecording, VoiceBroadcastRecordingsStoreEvent } from "../../voice-broadcast";
import { SDKContext } from "../../contexts/SDKContext"; import { SDKContext } from "../../contexts/SDKContext";
import { shouldShowFeedback } from "../../utils/Feedback"; import { shouldShowFeedback } from "../../utils/Feedback";
import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg"; import DarkLightModeSvg from "../../../res/img/element-icons/roomlist/dark-light-mode.svg";
@ -58,7 +56,6 @@ interface IState {
isDarkTheme: boolean; isDarkTheme: boolean;
isHighContrast: boolean; isHighContrast: boolean;
selectedSpace?: Room | null; selectedSpace?: Room | null;
showLiveAvatarAddon: boolean;
} }
const toRightOf = (rect: PartialDOMRect): MenuProps => { const toRightOf = (rect: PartialDOMRect): MenuProps => {
@ -79,7 +76,7 @@ const below = (rect: PartialDOMRect): MenuProps => {
export default class UserMenu extends React.Component<IProps, IState> { export default class UserMenu extends React.Component<IProps, IState> {
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private themeWatcherRef?: string; private themeWatcherRef?: string;
@ -94,7 +91,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
isDarkTheme: this.isUserOnDarkTheme(), isDarkTheme: this.isUserOnDarkTheme(),
isHighContrast: this.isUserOnHighContrastTheme(), isHighContrast: this.isUserOnHighContrastTheme(),
selectedSpace: SpaceStore.instance.activeSpaceRoom, selectedSpace: SpaceStore.instance.activeSpaceRoom,
showLiveAvatarAddon: this.context.voiceBroadcastRecordingsStore.hasCurrent(),
}; };
} }
@ -102,19 +98,9 @@ export default class UserMenu extends React.Component<IProps, IState> {
return !!getHomePageUrl(SdkConfig.get(), this.context.client!); return !!getHomePageUrl(SdkConfig.get(), this.context.client!);
} }
private onCurrentVoiceBroadcastRecordingChanged = (recording: VoiceBroadcastRecording | null): void => {
this.setState({
showLiveAvatarAddon: recording !== null,
});
};
public componentDidMount(): void { public componentDidMount(): void {
OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.on(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.on(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
);
this.dispatcherRef = defaultDispatcher.register(this.onAction); this.dispatcherRef = defaultDispatcher.register(this.onAction);
this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged); this.themeWatcherRef = SettingsStore.watchSetting("theme", null, this.onThemeChanged);
} }
@ -125,10 +111,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate); OwnProfileStore.instance.off(UPDATE_EVENT, this.onProfileUpdate);
SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate); SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdate);
this.context.voiceBroadcastRecordingsStore.off(
VoiceBroadcastRecordingsStoreEvent.CurrentChanged,
this.onCurrentVoiceBroadcastRecordingChanged,
);
} }
private isUserOnDarkTheme(): boolean { private isUserOnDarkTheme(): boolean {
@ -435,12 +417,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
name = <div className="mx_UserMenu_name">{displayName}</div>; name = <div className="mx_UserMenu_name">{displayName}</div>;
} }
const liveAvatarAddon = this.state.showLiveAvatarAddon ? (
<div className="mx_UserMenu_userAvatarLive" data-testid="user-menu-live-vb">
<LiveIcon className="mx_Icon_8" />
</div>
) : null;
return ( return (
<div className="mx_UserMenu"> <div className="mx_UserMenu">
<ContextMenuButton <ContextMenuButton
@ -459,7 +435,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
size={avatarSize + "px"} size={avatarSize + "px"}
className="mx_UserMenu_userAvatar_BaseAvatar" className="mx_UserMenu_userAvatar_BaseAvatar"
/> />
{liveAvatarAddon}
</div> </div>
{name} {name}
{this.renderContextMenu()} {this.renderContextMenu()}

View file

@ -32,7 +32,7 @@ interface IState {
export default class UserView extends React.Component<IProps, IState> { export default class UserView extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -82,7 +82,7 @@ export default class UserView extends React.Component<IProps, IState> {
} else if (this.state.member) { } else if (this.state.member) {
const panel = ( const panel = (
<RightPanel <RightPanel
overwriteCard={{ phase: RightPanelPhases.RoomMemberInfo, state: { member: this.state.member } }} overwriteCard={{ phase: RightPanelPhases.MemberInfo, state: { member: this.state.member } }}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
/> />
); );

View file

@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details.
import React, { RefObject } from "react"; import React, { RefObject } from "react";
import { MatrixEvent } from "matrix-js-sdk/src/matrix"; import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { useRoomContext } from "../../contexts/RoomContext";
import ResizeNotifier from "../../utils/ResizeNotifier"; import ResizeNotifier from "../../utils/ResizeNotifier";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
@ -19,6 +18,7 @@ import NewRoomIntro from "../views/rooms/NewRoomIntro";
import { UnwrappedEventTile } from "../views/rooms/EventTile"; import { UnwrappedEventTile } from "../views/rooms/EventTile";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext.tsx";
interface Props { interface Props {
roomView: RefObject<HTMLElement>; roomView: RefObject<HTMLElement>;
@ -32,7 +32,7 @@ interface Props {
* To avoid UTDs, users are shown a waiting room until the others have joined. * To avoid UTDs, users are shown a waiting room until the others have joined.
*/ */
export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => { export const WaitingForThirdPartyRoomView: React.FC<Props> = ({ roomView, resizeNotifier, inviteEvent }) => {
const context = useRoomContext(); const context = useScopedRoomContext("room");
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
return ( return (

View file

@ -64,7 +64,7 @@ interface IState {
export default class SoftLogout extends React.Component<IProps, IState> { export default class SoftLogout extends React.Component<IProps, IState> {
public static contextType = SDKContext; public static contextType = SDKContext;
public declare context: React.ContextType<typeof SDKContext>; declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) { public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props, context); super(props, context);

View file

@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
import { BaseGrouper } from "./BaseGrouper"; import { BaseGrouper } from "./BaseGrouper";
import MessagePanel, { WrappedEvent } from "../MessagePanel"; import MessagePanel, { WrappedEvent } from "../MessagePanel";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import DateSeparator from "../../views/messages/DateSeparator"; import DateSeparator from "../../views/messages/DateSeparator";
@ -53,11 +52,6 @@ export class CreationGrouper extends BaseGrouper {
return false; return false;
} }
if (VoiceBroadcastInfoEventType === eventType) {
// always show voice broadcast info events in timeline
return false;
}
if (event.isState() && event.getSender() === createEvent.getSender()) { if (event.isState() && event.getSender() === createEvent.getSender()) {
return true; return true;
} }

View file

@ -1,45 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React, { MutableRefObject } from "react";
import { toLeftOrRightOf } from "../../structures/ContextMenu";
import IconizedContextMenu, {
IconizedContextMenuOptionList,
IconizedContextMenuRadio,
} from "../context_menus/IconizedContextMenu";
interface Props {
containerRef: MutableRefObject<HTMLElement | null>;
currentDevice: MediaDeviceInfo | null;
devices: MediaDeviceInfo[];
onDeviceSelect: (device: MediaDeviceInfo) => void;
}
export const DevicesContextMenu: React.FC<Props> = ({ containerRef, currentDevice, devices, onDeviceSelect }) => {
const deviceOptions = devices.map((d: MediaDeviceInfo) => {
return (
<IconizedContextMenuRadio
key={d.deviceId}
active={d.deviceId === currentDevice?.deviceId}
onClick={() => onDeviceSelect(d)}
label={d.label}
/>
);
});
return (
<IconizedContextMenu
mountAsChild={false}
onFinished={() => {}}
{...(containerRef.current ? toLeftOrRightOf(containerRef.current.getBoundingClientRect(), 0) : {})}
>
<IconizedContextMenuOptionList>{deviceOptions}</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
};

View file

@ -108,12 +108,9 @@ export default class LoginWithQR extends React.Component<IProps, IState> {
private generateAndShowCode = async (): Promise<void> => { private generateAndShowCode = async (): Promise<void> => {
let rendezvous: MSC4108SignInWithQR; let rendezvous: MSC4108SignInWithQR;
try { try {
const fallbackRzServer = this.props.client?.getClientWellKnown()?.["io.element.rendezvous"]?.server;
const transport = new MSC4108RendezvousSession({ const transport = new MSC4108RendezvousSession({
onFailure: this.onFailure, onFailure: this.onFailure,
client: this.props.client, client: this.props.client,
fallbackRzServer,
}); });
await transport.send(""); await transport.send("");
const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure); const channel = new MSC4108SecureChannel(transport, undefined, this.onFailure);

View file

@ -16,10 +16,10 @@ import { Avatar } from "@vector-im/compound-web";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { ButtonEvent } from "../elements/AccessibleButton"; import { ButtonEvent } from "../elements/AccessibleButton";
import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter"; import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
name?: React.ComponentProps<typeof Avatar>["name"]; // The name (first initial used as default) name?: React.ComponentProps<typeof Avatar>["name"]; // The name (first initial used as default)
@ -57,8 +57,8 @@ const calculateUrls = (url?: string | null, urls?: string[], lowBandwidth = fals
const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => { const useImageUrl = ({ url, urls }: { url?: string | null; urls?: string[] }): [string, () => void] => {
// Since this is a hot code path and the settings store can be slow, we // Since this is a hot code path and the settings store can be slow, we
// use the cached lowBandwidth value from the room context if it exists // use the cached lowBandwidth value from the room context if it exists
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("lowBandwidth");
const lowBandwidth = roomContext ? roomContext.lowBandwidth : SettingsStore.getValue("lowBandwidth"); const lowBandwidth = roomContext?.lowBandwidth ?? SettingsStore.getValue("lowBandwidth");
const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth)); const [imageUrls, setUrls] = useState<string[]>(calculateUrls(url, urls, lowBandwidth));
const [urlsIndex, setIndex] = useState<number>(0); const [urlsIndex, setIndex] = useState<number>(0);

View file

@ -38,7 +38,7 @@ import ContextMenu, { toRightOf, MenuProps } from "../../structures/ContextMenu"
import ReactionPicker from "../emojipicker/ReactionPicker"; import ReactionPicker from "../emojipicker/ReactionPicker";
import ViewSource from "../../structures/ViewSource"; import ViewSource from "../../structures/ViewSource";
import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog"; import { createRedactEventDialog } from "../dialogs/ConfirmRedactDialog";
import ShareDialog from "../dialogs/ShareDialog"; import { ShareDialog } from "../dialogs/ShareDialog";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import EndPollDialog from "../dialogs/EndPollDialog"; import EndPollDialog from "../dialogs/EndPollDialog";
import { isPollEnded } from "../messages/MPollBody"; import { isPollEnded } from "../messages/MPollBody";
@ -126,7 +126,7 @@ interface IState {
export default class MessageContextMenu extends React.Component<IProps, IState> { export default class MessageContextMenu extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private reactButtonRef = createRef<any>(); // XXX Ref to a functional component private reactButtonRef = createRef<any>(); // XXX Ref to a functional component

View file

@ -18,7 +18,6 @@ import { _t } from "../../../languageHandler";
import { isAppWidget } from "../../../stores/WidgetStore"; import { isAppWidget } from "../../../stores/WidgetStore";
import WidgetUtils from "../../../utils/WidgetUtils"; import WidgetUtils from "../../../utils/WidgetUtils";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import RoomContext from "../../../contexts/RoomContext";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
@ -30,6 +29,7 @@ import { Container, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayo
import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream"; import { getConfigLivestreamUrl, startJitsiAudioLivestream } from "../../../Livestream";
import { ModuleRunner } from "../../../modules/ModuleRunner"; import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ElementWidget } from "../../../stores/widgets/StopGapWidget"; import { ElementWidget } from "../../../stores/widgets/StopGapWidget";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> { interface IProps extends Omit<ComponentProps<typeof IconizedContextMenu>, "children"> {
app: IWidget; app: IWidget;
@ -114,7 +114,7 @@ export const WidgetContextMenu: React.FC<IProps> = ({
...props ...props
}) => { }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const { room, roomId } = useContext(RoomContext); const { room, roomId } = useScopedRoomContext("room", "roomId");
const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app)); const widgetMessaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(app));
const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId); const canModify = userWidget || WidgetUtils.canUserModifyWidgets(cli, roomId);

View file

@ -1,21 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { _t } from "../../../languageHandler";
import Modal from "../../../Modal";
import InfoDialog from "./InfoDialog";
export const createCantStartVoiceMessageBroadcastDialog = (): void => {
Modal.createDialog(InfoDialog, {
title: _t("voice_message|cant_start_broadcast_title"),
description: <p>{_t("voice_message|cant_start_broadcast_description")}</p>,
hasCloseButton: true,
});
};

View file

@ -6,14 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature"; import { IRedactOpts, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { IRedactOpts, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import React from "react"; import React from "react";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import { isVoiceBroadcastStartedEvent } from "../../../voice-broadcast/utils/isVoiceBroadcastStartedEvent";
import ErrorDialog from "./ErrorDialog"; import ErrorDialog from "./ErrorDialog";
import TextInputDialog from "./TextInputDialog"; import TextInputDialog from "./TextInputDialog";
@ -70,18 +68,6 @@ export function createRedactEventDialog({
const cli = MatrixClientPeg.safeGet(); const cli = MatrixClientPeg.safeGet();
const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {}; const withRelTypes: Pick<IRedactOpts, "with_rel_types"> = {};
// redact related events if this is a voice broadcast started event and
// server has support for relation based redactions
if (isVoiceBroadcastStartedEvent(mxEvent)) {
const relationBasedRedactionsSupport = cli.canSupport.get(Feature.RelationBasedRedactions);
if (
relationBasedRedactionsSupport &&
relationBasedRedactionsSupport !== ServerSupport.Unsupported
) {
withRelTypes.with_rel_types = [RelationType.Reference];
}
}
try { try {
onCloseDialog?.(); onCloseDialog?.();
await cli.redactEvent(roomId, eventId, undefined, { await cli.redactEvent(roomId, eventId, undefined, {

View file

@ -22,7 +22,6 @@ import { AccountDataExplorer, RoomAccountDataExplorer } from "./devtools/Account
import SettingsFlag from "../elements/SettingsFlag"; import SettingsFlag from "../elements/SettingsFlag";
import { SettingLevel } from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import ServerInfo from "./devtools/ServerInfo"; import ServerInfo from "./devtools/ServerInfo";
import { Features } from "../../../settings/Settings";
import CopyableText from "../elements/CopyableText"; import CopyableText from "../elements/CopyableText";
import RoomNotifications from "./devtools/RoomNotifications"; import RoomNotifications from "./devtools/RoomNotifications";
@ -100,7 +99,6 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
<SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="developerMode" level={SettingLevel.ACCOUNT} />
<SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} /> <SettingsFlag name="showHiddenEventsInTimeline" level={SettingLevel.DEVICE} />
<SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} /> <SettingsFlag name="enableWidgetScreenshots" level={SettingLevel.ACCOUNT} />
<SettingsFlag name={Features.VoiceBroadcastForceSmallChunks} level={SettingLevel.DEVICE} />
</div> </div>
</BaseTool> </BaseTool>
); );

View file

@ -7,22 +7,23 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import * as React from "react"; import React, { JSX, useMemo, useRef, useState } from "react";
import { Room, RoomMember, MatrixEvent, User } from "matrix-js-sdk/src/matrix"; import { Room, RoomMember, MatrixEvent, User } from "matrix-js-sdk/src/matrix";
import { Checkbox, Button } from "@vector-im/compound-web";
import LinkIcon from "@vector-im/compound-design-tokens/assets/web/icons/link";
import CheckIcon from "@vector-im/compound-design-tokens/assets/web/icons/check";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import QRCode from "../elements/QRCode"; import QRCode from "../elements/QRCode";
import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator, makeUserPermalink } from "../../../utils/permalinks/Permalinks";
import { selectText } from "../../../utils/strings"; import { copyPlaintext } from "../../../utils/strings";
import StyledCheckbox from "../elements/StyledCheckbox";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import CopyableText from "../elements/CopyableText";
import { XOR } from "../../../@types/common"; import { XOR } from "../../../@types/common";
import { useSettingValue } from "../../../hooks/useSettings.ts";
/* eslint-disable @typescript-eslint/no-require-imports */ /* eslint-disable @typescript-eslint/no-require-imports */
const socials = [ const SOCIALS = [
{ {
name: "Facebook", name: "Facebook",
img: require("../../../../res/img/social/facebook.png"), img: require("../../../../res/img/social/facebook.png"),
@ -33,11 +34,7 @@ const socials = [
img: require("../../../../res/img/social/twitter-2.png"), img: require("../../../../res/img/social/twitter-2.png"),
url: (url: string) => `https://twitter.com/home?status=${url}`, url: (url: string) => `https://twitter.com/home?status=${url}`,
}, },
/* // icon missing {
name: 'Google Plus',
img: 'img/social/',
url: (url) => `https://plus.google.com/share?url=${url}`,
},*/ {
name: "LinkedIn", name: "LinkedIn",
img: require("../../../../res/img/social/linkedin.png"), img: require("../../../../res/img/social/linkedin.png"),
url: (url: string) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`, url: (url: string) => `https://www.linkedin.com/shareArticle?mini=true&url=${url}`,
@ -78,160 +75,153 @@ interface Props extends BaseProps {
* A <u>matrix.to</u> link will be generated out of it if it's not already a url. * A <u>matrix.to</u> link will be generated out of it if it's not already a url.
*/ */
target: Room | User | RoomMember | URL; target: Room | User | RoomMember | URL;
/**
* Optional when the target is a Room, User, RoomMember or a URL.
* Mandatory when the target is a MatrixEvent.
*/
permalinkCreator?: RoomPermalinkCreator; permalinkCreator?: RoomPermalinkCreator;
} }
interface EventProps extends BaseProps { interface EventProps extends BaseProps {
/**
* The target to link to.
*/
target: MatrixEvent; target: MatrixEvent;
/**
* Optional when the target is a Room, User, RoomMember or a URL.
* Mandatory when the target is a MatrixEvent.
*/
permalinkCreator: RoomPermalinkCreator; permalinkCreator: RoomPermalinkCreator;
} }
interface IState { type ShareDialogProps = XOR<Props, EventProps>;
linkSpecificEvent: boolean;
permalinkCreator: RoomPermalinkCreator | null;
}
export default class ShareDialog extends React.PureComponent<XOR<Props, EventProps>, IState> { /**
public constructor(props: XOR<Props, EventProps>) { * A dialog to share a link to a room, user, room member or a matrix event.
super(props); */
export function ShareDialog({ target, customTitle, onFinished, permalinkCreator }: ShareDialogProps): JSX.Element {
const showQrCode = useSettingValue<boolean>(UIFeature.ShareQRCode);
const showSocials = useSettingValue<boolean>(UIFeature.ShareSocial);
let permalinkCreator: RoomPermalinkCreator | null = null; const timeoutIdRef = useRef<number>();
if (props.target instanceof Room) { const [isCopied, setIsCopied] = useState(false);
permalinkCreator = new RoomPermalinkCreator(props.target);
permalinkCreator.load();
}
this.state = { const [linkToSpecificEvent, setLinkToSpecificEvent] = useState(target instanceof MatrixEvent);
// MatrixEvent defaults to share linkSpecificEvent const { title, url, checkboxLabel } = useTargetValues(target, linkToSpecificEvent, permalinkCreator);
linkSpecificEvent: this.props.target instanceof MatrixEvent, const newTitle = customTitle ?? title;
permalinkCreator,
};
}
public static onLinkClick(e: React.MouseEvent): void {
e.preventDefault();
selectText(e.currentTarget);
}
private onLinkSpecificEventCheckboxClick = (): void => {
this.setState({
linkSpecificEvent: !this.state.linkSpecificEvent,
});
};
private getUrl(): string {
if (this.props.target instanceof URL) {
return this.props.target.toString();
} else if (this.props.target instanceof Room) {
if (this.state.linkSpecificEvent) {
const events = this.props.target.getLiveTimeline().getEvents();
return this.state.permalinkCreator!.forEvent(events[events.length - 1].getId()!);
} else {
return this.state.permalinkCreator!.forShareableRoom();
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
return makeUserPermalink(this.props.target.userId);
} else if (this.state.linkSpecificEvent) {
return this.props.permalinkCreator!.forEvent(this.props.target.getId()!);
} else {
return this.props.permalinkCreator!.forShareableRoom();
}
}
public render(): React.ReactNode {
let title: string | undefined;
let checkbox: JSX.Element | undefined;
if (this.props.target instanceof URL) {
title = this.props.customTitle ?? _t("share|title_link");
} else if (this.props.target instanceof Room) {
title = this.props.customTitle ?? _t("share|title_room");
const events = this.props.target.getLiveTimeline().getEvents();
if (events.length > 0) {
checkbox = (
<div>
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick}
>
{_t("share|permalink_most_recent")}
</StyledCheckbox>
</div>
);
}
} else if (this.props.target instanceof User || this.props.target instanceof RoomMember) {
title = this.props.customTitle ?? _t("share|title_user");
} else if (this.props.target instanceof MatrixEvent) {
title = this.props.customTitle ?? _t("share|title_message");
checkbox = (
<div>
<StyledCheckbox
checked={this.state.linkSpecificEvent}
onChange={this.onLinkSpecificEventCheckboxClick}
>
{_t("share|permalink_message")}
</StyledCheckbox>
</div>
);
}
const matrixToUrl = this.getUrl();
const encodedUrl = encodeURIComponent(matrixToUrl);
const showQrCode = SettingsStore.getValue(UIFeature.ShareQRCode);
const showSocials = SettingsStore.getValue(UIFeature.ShareSocial);
let qrSocialSection;
if (showQrCode || showSocials) {
qrSocialSection = (
<>
<hr />
<div className="mx_ShareDialog_split">
{showQrCode && (
<div className="mx_ShareDialog_qrcode_container">
<QRCode data={matrixToUrl} width={256} />
</div>
)}
{showSocials && (
<div className="mx_ShareDialog_social_container">
{socials.map((social) => (
<a
rel="noreferrer noopener"
target="_blank"
key={social.name}
title={social.name}
href={social.url(encodedUrl)}
className="mx_ShareDialog_social_icon"
>
<img src={social.img} alt={social.name} height={64} width={64} />
</a>
))}
</div>
)}
</div>
</>
);
}
return ( return (
<BaseDialog <BaseDialog
title={title} title={newTitle}
className="mx_ShareDialog" className="mx_ShareDialog"
contentId="mx_Dialog_content" contentId="mx_Dialog_content"
onFinished={this.props.onFinished} onFinished={onFinished}
fixedWidth={false}
> >
{this.props.subtitle && <p>{this.props.subtitle}</p>}
<div className="mx_ShareDialog_content"> <div className="mx_ShareDialog_content">
<CopyableText getTextToCopy={() => matrixToUrl}> <div className="mx_ShareDialog_top">
<a title={_t("share|link_title")} href={matrixToUrl} onClick={ShareDialog.onLinkClick}> {showQrCode && <QRCode data={url} width={200} />}
{matrixToUrl} <span>{url}</span>
</a> </div>
</CopyableText> {checkboxLabel && (
{checkbox} <label>
{qrSocialSection} <Checkbox
defaultChecked={linkToSpecificEvent}
onChange={(evt) => setLinkToSpecificEvent(evt.target.checked)}
/>
{checkboxLabel}
</label>
)}
<Button
Icon={isCopied ? CheckIcon : LinkIcon}
onClick={async () => {
clearTimeout(timeoutIdRef.current);
await copyPlaintext(url);
setIsCopied(true);
timeoutIdRef.current = setTimeout(() => setIsCopied(false), 2000);
}}
>
{isCopied ? _t("share|link_copied") : _t("action|copy_link")}
</Button>
{showSocials && <SocialLinks url={url} />}
</div> </div>
</BaseDialog> </BaseDialog>
); );
} }
/**
* Social links to share the link on different platforms.
*/
interface SocialLinksProps {
/**
* The URL to share.
*/
url: string;
}
/**
* The socials to share the link on.
*/
function SocialLinks({ url }: SocialLinksProps): JSX.Element {
return (
<div className="mx_ShareDialog_social">
{SOCIALS.map((social) => (
<a
key={social.name}
href={social.url(url)}
target="_blank"
rel="noreferrer noopener"
title={social.name}
>
<img src={social.img} alt={social.name} />
</a>
))}
</div>
);
}
/**
* Get the title, url and checkbox label for the dialog based on the target.
* @param target
* @param linkToSpecificEvent
* @param permalinkCreator
*/
function useTargetValues(
target: ShareDialogProps["target"],
linkToSpecificEvent: boolean,
permalinkCreator?: RoomPermalinkCreator,
): { title: string; url: string; checkboxLabel?: string } {
return useMemo(() => {
if (target instanceof URL) return { title: _t("share|title_link"), url: target.toString() };
if (target instanceof User || target instanceof RoomMember)
return {
title: _t("share|title_user"),
url: makeUserPermalink(target.userId),
};
if (target instanceof Room) {
const title = _t("share|title_room");
const newPermalinkCreator = new RoomPermalinkCreator(target);
newPermalinkCreator.load();
const events = target.getLiveTimeline().getEvents();
return {
title,
url: linkToSpecificEvent
? newPermalinkCreator.forEvent(events[events.length - 1].getId()!)
: newPermalinkCreator.forShareableRoom(),
...(events.length > 0 && { checkboxLabel: _t("share|permalink_most_recent") }),
};
}
// MatrixEvent is remaining and should have a permalinkCreator
const url = linkToSpecificEvent
? permalinkCreator!.forEvent(target.getId()!)
: permalinkCreator!.forShareableRoom();
return {
title: _t("share|title_message"),
url,
checkboxLabel: _t("share|permalink_message"),
};
}, [target, linkToSpecificEvent, permalinkCreator]);
} }

View file

@ -116,7 +116,7 @@ interface IState {
export default class AppTile extends React.Component<IProps, IState> { export default class AppTile extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: ContextType<typeof MatrixClientContext>; declare public context: ContextType<typeof MatrixClientContext>;
public static defaultProps: Partial<IProps> = { public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true, waitForIframeLoad: true,

View file

@ -73,7 +73,7 @@ export default class EventListSummary extends React.Component<
IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">> IProps & Required<Pick<IProps, "summaryLength" | "threshold" | "avatarsMaxLength" | "layout">>
> { > {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public static defaultProps = { public static defaultProps = {
summaryLength: 1, summaryLength: 1,

View file

@ -12,12 +12,12 @@ import React, { useContext, useRef, useState, MouseEvent, ReactNode } from "reac
import { Tooltip } from "@vector-im/compound-web"; import { Tooltip } from "@vector-im/compound-web";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RoomContext from "../../../contexts/RoomContext";
import { useTimeout } from "../../../hooks/useTimeout"; import { useTimeout } from "../../../hooks/useTimeout";
import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds"; import { chromeFileInputFix } from "../../../utils/BrowserWorkarounds";
import AccessibleButton from "./AccessibleButton"; import AccessibleButton from "./AccessibleButton";
import Spinner from "./Spinner"; import Spinner from "./Spinner";
import { getFileChanged } from "../settings/AvatarSetting.tsx"; import { getFileChanged } from "../settings/AvatarSetting.tsx";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
export const AVATAR_SIZE = "52px"; export const AVATAR_SIZE = "52px";
@ -56,7 +56,7 @@ const MiniAvatarUploader: React.FC<IProps> = ({
const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel; const label = hasAvatar || busy ? hasAvatarLabel : noAvatarLabel;
const { room } = useContext(RoomContext); const { room } = useScopedRoomContext("room");
const canSetAvatar = const canSetAvatar =
isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId()); isUserAvatar || room?.currentState?.maySendStateEvent(EventType.RoomAvatar, cli.getSafeUserId());
if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>; if (!canSetAvatar) return <React.Fragment>{children}</React.Fragment>;

View file

@ -25,7 +25,7 @@ interface IProps {
export default class PersistentApp extends React.Component<IProps> { export default class PersistentApp extends React.Component<IProps> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: ContextType<typeof MatrixClientContext>; declare public context: ContextType<typeof MatrixClientContext>;
private room: Room; private room: Room;
public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {

View file

@ -65,7 +65,7 @@ interface IState {
// be low as each event being loaded (after the first) is triggered by an explicit user action. // be low as each event being loaded (after the first) is triggered by an explicit user action.
export default class ReplyChain extends React.Component<IProps, IState> { export default class ReplyChain extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private unmounted = false; private unmounted = false;
private room: Room; private room: Room;

View file

@ -33,7 +33,7 @@ interface IState {
// Controlled form component wrapping Field for inputting a room alias scoped to a given domain // Controlled form component wrapping Field for inputting a room alias scoped to a given domain
export default class RoomAliasField extends React.PureComponent<IProps, IState> { export default class RoomAliasField extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private fieldRef = createRef<Field>(); private fieldRef = createRef<Field>();

View file

@ -29,7 +29,7 @@ interface IState {
class ReactionPicker extends React.Component<IProps, IState> { class ReactionPicker extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context); super(props, context);

View file

@ -23,7 +23,7 @@ interface IProps {
class Search extends React.PureComponent<IProps> { class Search extends React.PureComponent<IProps> {
public static contextType = RovingTabIndexContext; public static contextType = RovingTabIndexContext;
public declare context: React.ContextType<typeof RovingTabIndexContext>; declare public context: React.ContextType<typeof RovingTabIndexContext>;
private inputRef = React.createRef<HTMLInputElement>(); private inputRef = React.createRef<HTMLInputElement>();

View file

@ -42,7 +42,7 @@ const isSharingOwnLocation = (shareType: LocationShareType): boolean =>
class LocationPicker extends React.Component<ILocationPickerProps, IState> { class LocationPicker extends React.Component<ILocationPickerProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private map?: maplibregl.Map; private map?: maplibregl.Map;
private geolocate?: maplibregl.GeolocateControl; private geolocate?: maplibregl.GeolocateControl;
private marker?: maplibregl.Marker; private marker?: maplibregl.Marker;

View file

@ -45,7 +45,7 @@ interface IState {
export default class EditHistoryMessage extends React.PureComponent<IProps, IState> { export default class EditHistoryMessage extends React.PureComponent<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private content = createRef<HTMLDivElement>(); private content = createRef<HTMLDivElement>();
private pills = new ReactRootManager(); private pills = new ReactRootManager();

View file

@ -30,7 +30,7 @@ interface IState {
export default class MAudioBody extends React.PureComponent<IBodyProps, IState> { export default class MAudioBody extends React.PureComponent<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public state: IState = {}; public state: IState = {};

View file

@ -102,7 +102,7 @@ interface IState {
export default class MFileBody extends React.Component<IProps, IState> { export default class MFileBody extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public state: IState = {}; public state: IState = {};

View file

@ -51,13 +51,14 @@ interface IState {
naturalHeight: number; naturalHeight: number;
}; };
hover: boolean; hover: boolean;
focus: boolean;
showImage: boolean; showImage: boolean;
placeholder: Placeholder; placeholder: Placeholder;
} }
export default class MImageBody extends React.Component<IBodyProps, IState> { export default class MImageBody extends React.Component<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private unmounted = false; private unmounted = false;
private image = createRef<HTMLImageElement>(); private image = createRef<HTMLImageElement>();
@ -71,6 +72,7 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
imgError: false, imgError: false,
imgLoaded: false, imgLoaded: false,
hover: false, hover: false,
focus: false,
showImage: SettingsStore.getValue("showImages"), showImage: SettingsStore.getValue("showImages"),
placeholder: Placeholder.NoImage, placeholder: Placeholder.NoImage,
}; };
@ -120,30 +122,29 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
}; };
protected onImageEnter = (e: React.MouseEvent<HTMLImageElement>): void => { private get shouldAutoplay(): boolean {
this.setState({ hover: true }); return !(
if (
!this.state.contentUrl || !this.state.contentUrl ||
!this.state.showImage || !this.state.showImage ||
!this.state.isAnimated || !this.state.isAnimated ||
SettingsStore.getValue("autoplayGifs") SettingsStore.getValue("autoplayGifs")
) { );
return;
} }
const imgElement = e.currentTarget;
imgElement.src = this.state.contentUrl; protected onImageEnter = (): void => {
this.setState({ hover: true });
}; };
protected onImageLeave = (e: React.MouseEvent<HTMLImageElement>): void => { protected onImageLeave = (): void => {
this.setState({ hover: false }); this.setState({ hover: false });
};
const url = this.state.thumbUrl ?? this.state.contentUrl; private onFocus = (): void => {
if (!url || !this.state.showImage || !this.state.isAnimated || SettingsStore.getValue("autoplayGifs")) { this.setState({ focus: true });
return; };
}
const imgElement = e.currentTarget; private onBlur = (): void => {
imgElement.src = url; this.setState({ focus: false });
}; };
private reconnectedListener = createReconnectedListener((): void => { private reconnectedListener = createReconnectedListener((): void => {
@ -470,14 +471,20 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
let showPlaceholder = Boolean(placeholder); let showPlaceholder = Boolean(placeholder);
const hoverOrFocus = this.state.hover || this.state.focus;
if (thumbUrl && !this.state.imgError) { if (thumbUrl && !this.state.imgError) {
let url = thumbUrl;
if (hoverOrFocus && this.shouldAutoplay) {
url = this.state.contentUrl!;
}
// Restrict the width of the thumbnail here, otherwise it will fill the container // Restrict the width of the thumbnail here, otherwise it will fill the container
// which has the same width as the timeline // which has the same width as the timeline
// mx_MImageBody_thumbnail resizes img to exactly container size // mx_MImageBody_thumbnail resizes img to exactly container size
img = ( img = (
<img <img
className="mx_MImageBody_thumbnail" className="mx_MImageBody_thumbnail"
src={thumbUrl} src={url}
ref={this.image} ref={this.image}
alt={content.body} alt={content.body}
onError={this.onImageError} onError={this.onImageError}
@ -493,13 +500,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. showPlaceholder = false; // because we're hiding the image, so don't show the placeholder.
} }
if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) { if (this.state.isAnimated && !SettingsStore.getValue("autoplayGifs") && !hoverOrFocus) {
// XXX: Arguably we may want a different label when the animated image is WEBP and not GIF // XXX: Arguably we may want a different label when the animated image is WEBP and not GIF
gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>; gifLabel = <p className="mx_MImageBody_gifLabel">GIF</p>;
} }
let banner: ReactNode | undefined; let banner: ReactNode | undefined;
if (this.state.showImage && this.state.hover) { if (this.state.showImage && hoverOrFocus) {
banner = this.getBanner(content); banner = this.getBanner(content);
} }
@ -568,7 +575,13 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode { protected wrapImage(contentUrl: string | null | undefined, children: JSX.Element): ReactNode {
if (contentUrl) { if (contentUrl) {
return ( return (
<a href={contentUrl} target={this.props.forExport ? "_blank" : undefined} onClick={this.onClick}> <a
href={contentUrl}
target={this.props.forExport ? "_blank" : undefined}
onClick={this.onClick}
onFocus={this.onFocus}
onBlur={this.onBlur}
>
{children} {children}
</a> </a>
); );
@ -657,17 +670,14 @@ export default class MImageBody extends React.Component<IBodyProps, IState> {
} }
interface PlaceholderIProps { interface PlaceholderIProps {
hover?: boolean;
maxWidth?: number; maxWidth?: number;
} }
export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> { export class HiddenImagePlaceholder extends React.PureComponent<PlaceholderIProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null; const maxWidth = this.props.maxWidth ? this.props.maxWidth + "px" : null;
let className = "mx_HiddenImagePlaceholder";
if (this.props.hover) className += " mx_HiddenImagePlaceholder_hover";
return ( return (
<div className={className} style={{ maxWidth: `min(100%, ${maxWidth}px)` }}> <div className="mx_HiddenImagePlaceholder" style={{ maxWidth: `min(100%, ${maxWidth}px)` }}>
<div className="mx_HiddenImagePlaceholder_button"> <div className="mx_HiddenImagePlaceholder_button">
<span className="mx_HiddenImagePlaceholder_eye" /> <span className="mx_HiddenImagePlaceholder_eye" />
<span>{_t("timeline|m.image|show_image")}</span> <span>{_t("timeline|m.image|show_image")}</span>

View file

@ -30,7 +30,7 @@ interface IState {
export default class MLocationBody extends React.Component<IBodyProps, IState> { export default class MLocationBody extends React.Component<IBodyProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private unmounted = false; private unmounted = false;
private mapId: string; private mapId: string;

View file

@ -139,7 +139,7 @@ export function launchPollEditor(mxEvent: MatrixEvent, getRelationsForEvent?: Ge
export default class MPollBody extends React.Component<IBodyProps, IState> { export default class MPollBody extends React.Component<IBodyProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
private seenEventIds: string[] = []; // Events we have already seen private seenEventIds: string[] = []; // Events we have already seen
public constructor(props: IBodyProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IBodyProps, context: React.ContextType<typeof MatrixClientContext>) {

View file

@ -34,7 +34,7 @@ interface IState {
export default class MVideoBody extends React.PureComponent<IBodyProps, IState> { export default class MVideoBody extends React.PureComponent<IBodyProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private videoRef = React.createRef<HTMLVideoElement>(); private videoRef = React.createRef<HTMLVideoElement>();
private sizeWatcher?: string; private sizeWatcher?: string;

View file

@ -58,7 +58,6 @@ import { ALTERNATE_KEY_NAME } from "../../../accessibility/KeyboardShortcuts";
import { Action } from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload"; import { ShowThreadPayload } from "../../../dispatcher/payloads/ShowThreadPayload";
import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile"; import { GetRelationsForEvent, IEventTileType } from "../rooms/EventTile";
import { VoiceBroadcastInfoEventType } from "../../../voice-broadcast/types";
import { ButtonEvent } from "../elements/AccessibleButton"; import { ButtonEvent } from "../elements/AccessibleButton";
import PinningUtils from "../../../utils/PinningUtils"; import PinningUtils from "../../../utils/PinningUtils";
import PosthogTrackers from "../../../PosthogTrackers.ts"; import PosthogTrackers from "../../../PosthogTrackers.ts";
@ -262,7 +261,7 @@ interface IMessageActionBarProps {
export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> { export default class MessageActionBar extends React.PureComponent<IMessageActionBarProps> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public componentDidMount(): void { public componentDidMount(): void {
if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) { if (this.props.mxEvent.status && this.props.mxEvent.status !== EventStatus.SENT) {
@ -354,8 +353,7 @@ export default class MessageActionBar extends React.PureComponent<IMessageAction
* until cross-platform support * until cross-platform support
* (PSF-1041) * (PSF-1041)
*/ */
!M_BEACON_INFO.matches(this.props.mxEvent.getType()) && !M_BEACON_INFO.matches(this.props.mxEvent.getType());
!(this.props.mxEvent.getType() === VoiceBroadcastInfoEventType);
return inNotThreadTimeline && isAllowedMessageType; return inNotThreadTimeline && isAllowedMessageType;
} }

View file

@ -41,7 +41,6 @@ import MjolnirBody from "./MjolnirBody";
import MBeaconBody from "./MBeaconBody"; import MBeaconBody from "./MBeaconBody";
import { DecryptionFailureBody } from "./DecryptionFailureBody"; import { DecryptionFailureBody } from "./DecryptionFailureBody";
import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile"; import { GetRelationsForEvent, IEventTileOps } from "../rooms/EventTile";
import { VoiceBroadcastBody, VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../voice-broadcast";
// onMessageAllowed is handled internally // onMessageAllowed is handled internally
interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> { interface IProps extends Omit<IBodyProps, "onMessageAllowed" | "mediaEventHelper"> {
@ -85,7 +84,7 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries()); private evTypes = new Map<string, React.ComponentType<IBodyProps>>(baseEvTypes.entries());
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) { public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
super(props, context); super(props, context);
@ -276,10 +275,6 @@ export default class MessageEvent extends React.Component<IProps> implements IMe
if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) { if (M_LOCATION.matches(type) || (type === EventType.RoomMessage && msgtype === MsgType.Location)) {
BodyType = MLocationBody; BodyType = MLocationBody;
} }
if (type === VoiceBroadcastInfoEventType && content?.state === VoiceBroadcastInfoState.Started) {
BodyType = VoiceBroadcastBody;
}
} }
if (SettingsStore.getValue("feature_mjolnir")) { if (SettingsStore.getValue("feature_mjolnir")) {

View file

@ -75,7 +75,7 @@ interface IState {
export default class ReactionsRow extends React.PureComponent<IProps, IState> { export default class ReactionsRow extends React.PureComponent<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context); super(props, context);

View file

@ -38,7 +38,7 @@ export interface IProps {
export default class ReactionsRowButton extends React.PureComponent<IProps> { export default class ReactionsRowButton extends React.PureComponent<IProps> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public onClick = (): void => { public onClick = (): void => {
const { mxEvent, myReactionEvent, content } = this.props; const { mxEvent, myReactionEvent, content } = this.props;

View file

@ -28,7 +28,7 @@ interface IProps {
export default class ReactionsRowButtonTooltip extends React.PureComponent<PropsWithChildren<IProps>> { export default class ReactionsRowButtonTooltip extends React.PureComponent<PropsWithChildren<IProps>> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: React.ContextType<typeof MatrixClientContext>; declare public context: React.ContextType<typeof MatrixClientContext>;
public render(): React.ReactNode { public render(): React.ReactNode {
const { content, reactionEvents, mxEvent, children } = this.props; const { content, reactionEvents, mxEvent, children } = this.props;

View file

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { useCallback, useContext } from "react"; import React, { useCallback } from "react";
import { logger } from "matrix-js-sdk/src/logger"; import { logger } from "matrix-js-sdk/src/logger";
import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix"; import { MatrixEvent, Room, RoomState } from "matrix-js-sdk/src/matrix";
@ -18,10 +18,10 @@ import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
import EventTileBubble from "./EventTileBubble"; import EventTileBubble from "./EventTileBubble";
import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import RoomContext from "../../../contexts/RoomContext";
import { useRoomState } from "../../../hooks/useRoomState"; import { useRoomState } from "../../../hooks/useRoomState";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor"; import MatrixToPermalinkConstructor from "../../../utils/permalinks/MatrixToPermalinkConstructor";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
/** The m.room.create MatrixEvent that this tile represents */ /** The m.room.create MatrixEvent that this tile represents */
@ -40,7 +40,7 @@ export const RoomPredecessorTile: React.FC<IProps> = ({ mxEvent, timestamp }) =>
// the information inside mxEvent. This allows us the flexibility later to // the information inside mxEvent. This allows us the flexibility later to
// use a different predecessor (e.g. through MSC3946) and still display it // use a different predecessor (e.g. through MSC3946) and still display it
// in the timeline location of the create event. // in the timeline location of the create event.
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("room");
const predecessor = useRoomState( const predecessor = useRoomState(
roomContext.room, roomContext.room,
useCallback( useCallback(

View file

@ -52,10 +52,8 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
private tooltips = new ReactRootManager(); private tooltips = new ReactRootManager();
private reactRoots = new ReactRootManager(); private reactRoots = new ReactRootManager();
private ref = createRef<HTMLDivElement>();
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public state = { public state = {
links: [], links: [],
@ -86,7 +84,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") { if (this.props.mxEvent.getContent().format === "org.matrix.custom.html") {
// Handle expansion and add buttons // Handle expansion and add buttons
const pres = this.ref.current?.getElementsByTagName("pre"); const pres = [...content.getElementsByTagName("pre")];
if (pres && pres.length > 0) { if (pres && pres.length > 0) {
for (let i = 0; i < pres.length; i++) { for (let i = 0; i < pres.length; i++) {
// If there already is a div wrapping the codeblock we want to skip this. // If there already is a div wrapping the codeblock we want to skip this.
@ -115,13 +113,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
root.className = "mx_EventTile_pre_container"; root.className = "mx_EventTile_pre_container";
// Insert containing div in place of <pre> block // Insert containing div in place of <pre> block
pre.parentNode?.replaceChild(root, pre); pre.replaceWith(root);
this.reactRoots.render( this.reactRoots.render(
<StrictMode> <StrictMode>
<CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock> <CodeBlock onHeightChanged={this.props.onHeightChanged}>{pre}</CodeBlock>
</StrictMode>, </StrictMode>,
root, root,
pre,
); );
} }
@ -196,10 +195,9 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
</StrictMode> </StrictMode>
); );
this.reactRoots.render(spoiler, spoilerContainer); this.reactRoots.render(spoiler, spoilerContainer, node);
node.parentNode?.replaceChild(spoilerContainer, node);
node.replaceWith(spoilerContainer);
node = spoilerContainer; node = spoilerContainer;
} }
@ -479,12 +477,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
if (isEmote) { if (isEmote) {
return ( return (
<div <div className="mx_MEmoteBody mx_EventTile_content" onClick={this.onBodyLinkClick} dir="auto">
className="mx_MEmoteBody mx_EventTile_content"
onClick={this.onBodyLinkClick}
dir="auto"
ref={this.ref}
>
*&nbsp; *&nbsp;
<span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}> <span className="mx_MEmoteBody_sender" onClick={this.onEmoteSenderClick}>
{mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()} {mxEvent.sender ? mxEvent.sender.name : mxEvent.getSender()}
@ -497,7 +490,7 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
if (isNotice) { if (isNotice) {
return ( return (
<div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}> <div className="mx_MNoticeBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body} {body}
{widgets} {widgets}
</div> </div>
@ -505,14 +498,14 @@ export default class TextualBody extends React.Component<IBodyProps, IState> {
} }
if (isCaption) { if (isCaption) {
return ( return (
<div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick} ref={this.ref}> <div className="mx_MTextBody mx_EventTile_caption" onClick={this.onBodyLinkClick}>
{body} {body}
{widgets} {widgets}
</div> </div>
); );
} }
return ( return (
<div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick} ref={this.ref}> <div className="mx_MTextBody mx_EventTile_content" onClick={this.onBodyLinkClick}>
{body} {body}
{widgets} {widgets}
</div> </div>

View file

@ -19,7 +19,7 @@ interface IProps {
export default class TextualEvent extends React.Component<IProps> { export default class TextualEvent extends React.Component<IProps> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public render(): React.ReactNode { public render(): React.ReactNode {
const text = TextForEvent.textForEvent( const text = TextForEvent.textForEvent(

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { useCallback, useEffect, JSX } from "react"; import React, { useCallback, useEffect, JSX, useContext } from "react";
import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix"; import { Room, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { Button, Separator } from "@vector-im/compound-web"; import { Button, Separator } from "@vector-im/compound-web";
import classNames from "classnames"; import classNames from "classnames";
@ -18,7 +18,7 @@ import Spinner from "../elements/Spinner";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext"; import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { PinnedEventTile } from "../rooms/PinnedEventTile"; import { PinnedEventTile } from "../rooms/PinnedEventTile";
import { useRoomState } from "../../../hooks/useRoomState"; import { useRoomState } from "../../../hooks/useRoomState";
import RoomContext, { TimelineRenderingType, useRoomContext } from "../../../contexts/RoomContext"; import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
import { ReadPinsEventId } from "./types"; import { ReadPinsEventId } from "./types";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { filterBoolean } from "../../../utils/arrays"; import { filterBoolean } from "../../../utils/arrays";
@ -27,6 +27,7 @@ import { UnpinAllDialog } from "../dialogs/UnpinAllDialog";
import EmptyState from "./EmptyState"; import EmptyState from "./EmptyState";
import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import { usePinnedEvents, useReadPinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents";
import PinningUtils from "../../../utils/PinningUtils.ts"; import PinningUtils from "../../../utils/PinningUtils.ts";
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx";
/** /**
* List the pinned messages in a room inside a Card. * List the pinned messages in a room inside a Card.
@ -48,7 +49,7 @@ interface PinnedMessagesCardProps {
export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element { export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMessagesCardProps): JSX.Element {
const cli = useMatrixClientContext(); const cli = useMatrixClientContext();
const roomContext = useRoomContext(); const roomContext = useContext(RoomContext);
const pinnedEventIds = usePinnedEvents(room); const pinnedEventIds = usePinnedEvents(room);
const readPinnedEvents = useReadPinnedEvents(room); const readPinnedEvents = useReadPinnedEvents(room);
const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds); const pinnedEvents = useSortedFetchedPinnedEvents(room, pinnedEventIds);
@ -89,14 +90,9 @@ export function PinnedMessagesCard({ room, onClose, permalinkCreator }: PinnedMe
className="mx_PinnedMessagesCard" className="mx_PinnedMessagesCard"
onClose={onClose} onClose={onClose}
> >
<RoomContext.Provider <ScopedRoomContextProvider {...roomContext} timelineRenderingType={TimelineRenderingType.Pinned}>
value={{
...roomContext,
timelineRenderingType: TimelineRenderingType.Pinned,
}}
>
{content} {content}
</RoomContext.Provider> </ScopedRoomContextProvider>
</BaseCard> </BaseCard>
); );
} }

View file

@ -47,11 +47,11 @@ import RoomAvatar from "../avatars/RoomAvatar";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import ShareDialog from "../dialogs/ShareDialog"; import { ShareDialog } from "../dialogs/ShareDialog";
import { useEventEmitterState } from "../../../hooks/useEventEmitter"; import { useEventEmitterState } from "../../../hooks/useEventEmitter";
import { E2EStatus } from "../../../utils/ShieldUtils"; import { E2EStatus } from "../../../utils/ShieldUtils";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext"; import { TimelineRenderingType } from "../../../contexts/RoomContext";
import RoomName from "../elements/RoomName"; import RoomName from "../elements/RoomName";
import ExportDialog from "../dialogs/ExportDialog"; import ExportDialog from "../dialogs/ExportDialog";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
@ -76,6 +76,7 @@ import { useTransition } from "../../../hooks/useTransition";
import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms";
import { usePinnedEvents } from "../../../hooks/usePinnedEvents"; import { usePinnedEvents } from "../../../hooks/usePinnedEvents";
import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx"; import { ReleaseAnnouncement } from "../../structures/ReleaseAnnouncement.tsx";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
room: Room; room: Room;
@ -86,7 +87,7 @@ interface IProps {
} }
const onRoomMembersClick = (): void => { const onRoomMembersClick = (): void => {
RightPanelStore.instance.pushCard({ phase: RightPanelPhases.RoomMemberList }, true); RightPanelStore.instance.pushCard({ phase: RightPanelPhases.MemberList }, true);
}; };
const onRoomThreadsClick = (): void => { const onRoomThreadsClick = (): void => {
@ -232,7 +233,7 @@ const RoomSummaryCard: React.FC<IProps> = ({
}; };
const isRoomEncrypted = useIsEncrypted(cli, room); const isRoomEncrypted = useIsEncrypted(cli, room);
const roomContext = useContext(RoomContext); const roomContext = useScopedRoomContext("e2eStatus", "timelineRenderingType");
const e2eStatus = roomContext.e2eStatus; const e2eStatus = roomContext.e2eStatus;
const isVideoRoom = calcIsVideoRoom(room); const isVideoRoom = calcIsVideoRoom(room);

View file

@ -38,6 +38,7 @@ import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import Measured from "../elements/Measured"; import Measured from "../elements/Measured";
import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import { SdkContextClass } from "../../../contexts/SDKContext"; import { SdkContextClass } from "../../../contexts/SDKContext";
import { ScopedRoomContextProvider } from "../../../contexts/ScopedRoomContext.tsx";
interface IProps { interface IProps {
room: Room; room: Room;
@ -68,7 +69,7 @@ interface IState {
export default class TimelineCard extends React.Component<IProps, IState> { export default class TimelineCard extends React.Component<IProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private dispatcherRef?: string; private dispatcherRef?: string;
private layoutWatcherRef?: string; private layoutWatcherRef?: string;
@ -199,13 +200,11 @@ export default class TimelineCard extends React.Component<IProps, IState> {
const showComposer = myMembership === KnownMembership.Join; const showComposer = myMembership === KnownMembership.Join;
return ( return (
<RoomContext.Provider <ScopedRoomContextProvider
value={{ {...this.context}
...this.context, timelineRenderingType={this.props.timelineRenderingType ?? this.context.timelineRenderingType}
timelineRenderingType: this.props.timelineRenderingType ?? this.context.timelineRenderingType, liveTimeline={this.props.timelineSet?.getLiveTimeline()}
liveTimeline: this.props.timelineSet?.getLiveTimeline(), narrow={this.state.narrow}
narrow: this.state.narrow,
}}
> >
<BaseCard <BaseCard
className={this.props.classNames} className={this.props.classNames}
@ -255,7 +254,7 @@ export default class TimelineCard extends React.Component<IProps, IState> {
/> />
)} )}
</BaseCard> </BaseCard>
</RoomContext.Provider> </ScopedRoomContextProvider>
); );
} }
} }

View file

@ -63,7 +63,7 @@ import PowerSelector from "../elements/PowerSelector";
import MemberAvatar from "../avatars/MemberAvatar"; import MemberAvatar from "../avatars/MemberAvatar";
import PresenceLabel from "../rooms/PresenceLabel"; import PresenceLabel from "../rooms/PresenceLabel";
import BulkRedactDialog from "../dialogs/BulkRedactDialog"; import BulkRedactDialog from "../dialogs/BulkRedactDialog";
import ShareDialog from "../dialogs/ShareDialog"; import { ShareDialog } from "../dialogs/ShareDialog";
import ErrorDialog from "../dialogs/ErrorDialog"; import ErrorDialog from "../dialogs/ErrorDialog";
import QuestionDialog from "../dialogs/QuestionDialog"; import QuestionDialog from "../dialogs/QuestionDialog";
import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog"; import ConfirmUserActionDialog from "../dialogs/ConfirmUserActionDialog";
@ -1739,13 +1739,13 @@ export const UserInfoHeader: React.FC<{
interface IProps { interface IProps {
user: Member; user: Member;
room?: Room; room?: Room;
phase: RightPanelPhases.RoomMemberInfo | RightPanelPhases.SpaceMemberInfo | RightPanelPhases.EncryptionPanel; phase: RightPanelPhases.MemberInfo | RightPanelPhases.EncryptionPanel;
onClose(): void; onClose(): void;
verificationRequest?: VerificationRequest; verificationRequest?: VerificationRequest;
verificationRequestPromise?: Promise<VerificationRequest>; verificationRequestPromise?: Promise<VerificationRequest>;
} }
const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPhases.RoomMemberInfo, ...props }) => { const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPhases.MemberInfo, ...props }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
// fetch latest room member if we have a room, so we don't show historical information, falling back to user // fetch latest room member if we have a room, so we don't show historical information, falling back to user
@ -1767,8 +1767,6 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
// We have no previousPhase for when viewing a UserInfo without a Room at this time // We have no previousPhase for when viewing a UserInfo without a Room at this time
if (room && phase === RightPanelPhases.EncryptionPanel) { if (room && phase === RightPanelPhases.EncryptionPanel) {
cardState = { member }; cardState = { member };
} else if (room?.isSpaceRoom()) {
cardState = { spaceId: room.roomId };
} }
const onEncryptionPanelClose = (): void => { const onEncryptionPanelClose = (): void => {
@ -1777,8 +1775,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
let content: JSX.Element | undefined; let content: JSX.Element | undefined;
switch (phase) { switch (phase) {
case RightPanelPhases.RoomMemberInfo: case RightPanelPhases.MemberInfo:
case RightPanelPhases.SpaceMemberInfo:
content = ( content = (
<BasicUserInfo <BasicUserInfo
room={room as Room} room={room as Room}
@ -1823,7 +1820,7 @@ const UserInfo: React.FC<IProps> = ({ user, room, onClose, phase = RightPanelPha
closeLabel={closeLabel} closeLabel={closeLabel}
cardState={cardState} cardState={cardState}
onBack={(ev: ButtonEvent) => { onBack={(ev: ButtonEvent) => {
if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.RoomMemberList) { if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) {
PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev); PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev);
} }
}} }}

View file

@ -94,7 +94,7 @@ interface IState {
export default class AliasSettings extends React.Component<IProps, IState> { export default class AliasSettings extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext; public static contextType = MatrixClientContext;
public declare context: ContextType<typeof MatrixClientContext>; declare public context: ContextType<typeof MatrixClientContext>;
public static defaultProps = { public static defaultProps = {
canSetAliases: false, canSetAliases: false,

View file

@ -49,7 +49,7 @@ export default class Autocomplete extends React.PureComponent<IProps, IState> {
private completionRefs: Record<string, RefObject<HTMLElement>> = {}; private completionRefs: Record<string, RefObject<HTMLElement>> = {};
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) { public constructor(props: IProps, context: React.ContextType<typeof RoomContext>) {
super(props, context); super(props, context);

View file

@ -43,25 +43,6 @@ import { attachMentions, attachRelation } from "./SendMessageComposer";
import { filterBoolean } from "../../../utils/arrays"; import { filterBoolean } from "../../../utils/arrays";
import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { MatrixClientPeg } from "../../../MatrixClientPeg";
function getHtmlReplyFallback(mxEvent: MatrixEvent): string {
const html = mxEvent.getContent().formatted_body;
if (!html) {
return "";
}
const rootNode = new DOMParser().parseFromString(html, "text/html").body;
const mxReply = rootNode.querySelector("mx-reply");
return (mxReply && mxReply.outerHTML) || "";
}
function getTextReplyFallback(mxEvent: MatrixEvent): string {
const body: string = mxEvent.getContent().body;
const lines = body.split("\n").map((l) => l.trim());
if (lines.length > 2 && lines[0].startsWith("> ") && lines[1].length === 0) {
return `${lines[0]}\n\n`;
}
return "";
}
// exported for tests // exported for tests
export function createEditContent( export function createEditContent(
model: EditorModel, model: EditorModel,
@ -72,15 +53,6 @@ export function createEditContent(
if (isEmote) { if (isEmote) {
model = stripEmoteCommand(model); model = stripEmoteCommand(model);
} }
const isReply = !!editedEvent.replyEventId;
let plainPrefix = "";
let htmlPrefix = "";
if (isReply) {
plainPrefix = getTextReplyFallback(editedEvent);
htmlPrefix = getHtmlReplyFallback(editedEvent);
}
const body = textSerialize(model); const body = textSerialize(model);
const newContent: RoomMessageEventContent = { const newContent: RoomMessageEventContent = {
@ -89,19 +61,18 @@ export function createEditContent(
}; };
const contentBody: RoomMessageTextEventContent & Omit<ReplacementEvent<RoomMessageEventContent>, "m.relates_to"> = { const contentBody: RoomMessageTextEventContent & Omit<ReplacementEvent<RoomMessageEventContent>, "m.relates_to"> = {
"msgtype": newContent.msgtype, "msgtype": newContent.msgtype,
"body": `${plainPrefix} * ${body}`, "body": `* ${body}`,
"m.new_content": newContent, "m.new_content": newContent,
}; };
const formattedBody = htmlSerializeIfNeeded(model, { const formattedBody = htmlSerializeIfNeeded(model, {
forceHTML: isReply,
useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"), useMarkdown: SettingsStore.getValue("MessageComposerInput.useMarkdown"),
}); });
if (formattedBody) { if (formattedBody) {
newContent.format = "org.matrix.custom.html"; newContent.format = "org.matrix.custom.html";
newContent.formatted_body = formattedBody; newContent.formatted_body = formattedBody;
contentBody.format = newContent.format; contentBody.format = newContent.format;
contentBody.formatted_body = `${htmlPrefix} * ${formattedBody}`; contentBody.formatted_body = `* ${formattedBody}`;
} }
// Build the mentions properties for both the content and new_content. // Build the mentions properties for both the content and new_content.
@ -121,7 +92,7 @@ interface IState {
class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> { class EditMessageComposer extends React.Component<IEditMessageComposerProps, IState> {
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private readonly editorRef = createRef<BasicMessageComposer>(); private readonly editorRef = createRef<BasicMessageComposer>();
private dispatcherRef?: string; private dispatcherRef?: string;

View file

@ -296,7 +296,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
}; };
public static contextType = RoomContext; public static contextType = RoomContext;
public declare context: React.ContextType<typeof RoomContext>; declare public context: React.ContextType<typeof RoomContext>;
private unmounted = false; private unmounted = false;
@ -758,8 +758,13 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender_key"); shieldReasonMessage = _t("encryption|event_shield_reason_mismatched_sender_key");
break; break;
default: case EventShieldReason.SENT_IN_CLEAR:
shieldReasonMessage = _t("error|unknown"); shieldReasonMessage = _t("common|unencrypted");
break;
case EventShieldReason.VERIFICATION_VIOLATION:
shieldReasonMessage = _t("timeline|decryption_failure|sender_identity_previously_verified");
break;
} }
if (this.state.shieldColour === EventShieldColour.GREY) { if (this.state.shieldColour === EventShieldColour.GREY) {
@ -770,7 +775,7 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
} }
} }
if (MatrixClientPeg.safeGet().isRoomEncrypted(ev.getRoomId()!)) { if (this.context.isRoomEncrypted) {
// else if room is encrypted // else if room is encrypted
// and event is being encrypted or is not_sent (Unknown Devices/Network Error) // and event is being encrypted or is not_sent (Unknown Devices/Network Error)
if (ev.status === EventStatus.ENCRYPTING) { if (ev.status === EventStatus.ENCRYPTING) {

View file

@ -6,15 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details. Please see LICENSE files in the repository root for full details.
*/ */
import React, { useContext } from "react"; import React from "react";
import { EventTimeline } from "matrix-js-sdk/src/matrix"; import { EventTimeline } from "matrix-js-sdk/src/matrix";
import EventTileBubble from "../messages/EventTileBubble"; import EventTileBubble from "../messages/EventTileBubble";
import RoomContext from "../../../contexts/RoomContext";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx";
const HistoryTile: React.FC = () => { const HistoryTile: React.FC = () => {
const { room } = useContext(RoomContext); const { room } = useScopedRoomContext("room");
const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS); const oldState = room?.getLiveTimeline().getState(EventTimeline.BACKWARDS);
const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility; const historyState = oldState?.getStateEvents("m.room.history_visibility")[0]?.getContent().history_visibility;

Some files were not shown because too many files have changed in this diff Show more