Compare commits
54 Commits
toger5/dev
...
hs/remove-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba6fcbc985 | ||
|
|
529e6beb02 | ||
|
|
8a1eb8b632 | ||
|
|
c81d600307 | ||
|
|
7df3b45ee3 | ||
|
|
f83bf60a14 | ||
|
|
2da1cd239f | ||
|
|
cdd41d8f5a | ||
|
|
fc49e0f281 | ||
|
|
9d04af146a | ||
|
|
31e48c4fc1 | ||
|
|
ddb2136d43 | ||
|
|
2c857f8fe3 | ||
|
|
b9020d78fb | ||
|
|
d47dd66736 | ||
|
|
01aaddf93e | ||
|
|
a4e5e77951 | ||
|
|
7e100f7a35 | ||
|
|
42e022e894 | ||
|
|
47a640d780 | ||
|
|
d0daf2c2af | ||
|
|
ff8e322072 | ||
|
|
2c416f7e7b | ||
|
|
782921f5ac | ||
|
|
452ff3b615 | ||
|
|
5982a4cfed | ||
|
|
cfb2f719fb | ||
|
|
5454c2bfbe | ||
|
|
b58492fa40 | ||
|
|
1a2aeb14c2 | ||
|
|
58f59b2d7f | ||
|
|
4a85b7c162 | ||
|
|
6ff4ac87ac | ||
|
|
c72ebed739 | ||
|
|
1ef5ba04e9 | ||
|
|
c59c724789 | ||
|
|
052cc07c13 | ||
|
|
0c40f53355 | ||
|
|
b4e09790b5 | ||
|
|
e06e42116a | ||
|
|
7723c0f708 | ||
|
|
a3f1a8c649 | ||
|
|
57e8e51821 | ||
|
|
b4c926f85c | ||
|
|
a413ae3f43 | ||
|
|
88e52601c8 | ||
|
|
51cb4a5cfd | ||
|
|
ffedde2509 | ||
|
|
03da3b55b5 | ||
|
|
206304b5d7 | ||
|
|
956d936235 | ||
|
|
ef4d9ea8b7 | ||
|
|
5aca14db74 | ||
|
|
8a9eb35bf9 |
@@ -1,11 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||||
extends: [
|
extends: ["plugin:matrix-org/babel", "plugin:matrix-org/react", "plugin:matrix-org/a11y"],
|
||||||
"plugin:matrix-org/babel",
|
|
||||||
"plugin:matrix-org/react",
|
|
||||||
"plugin:matrix-org/a11y",
|
|
||||||
"plugin:storybook/recommended",
|
|
||||||
],
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.json"],
|
project: ["./tsconfig.json"],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# Triggers after the shared component tests have finished,
|
|
||||||
# It uploads the received images and diffs to netlify, printing the URLs to the console
|
|
||||||
name: Upload Shared Component Visual Test Diffs
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
workflows: ["Shared Component Visual Tests"]
|
|
||||||
types:
|
|
||||||
- completed
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_branch || github.run_id }}
|
|
||||||
cancel-in-progress: ${{ github.event.workflow_run.event == 'pull_request' }}
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
report:
|
|
||||||
if: github.event.workflow_run.conclusion == 'failure'
|
|
||||||
name: Upload Diffs
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
environment: Netlify
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
deployments: write
|
|
||||||
steps:
|
|
||||||
- name: Install tree
|
|
||||||
run: "sudo apt-get install -y tree"
|
|
||||||
|
|
||||||
- name: Download Diffs
|
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
||||||
with:
|
|
||||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run-id: ${{ github.event.workflow_run.id }}
|
|
||||||
name: received-images
|
|
||||||
path: received-images
|
|
||||||
|
|
||||||
- name: Generate Index
|
|
||||||
run: "cd received-images && tree -L 1 --noreport -H '' -o index.html ."
|
|
||||||
|
|
||||||
- name: 📤 Deploy to Netlify
|
|
||||||
uses: matrix-org/netlify-pr-preview@9805cd123fc9a7e421e35340a05e1ebc5dee46b5 # v3
|
|
||||||
with:
|
|
||||||
path: received-images
|
|
||||||
owner: ${{ github.event.workflow_run.head_repository.owner.login }}
|
|
||||||
branch: ${{ github.event.workflow_run.head_branch }}
|
|
||||||
revision: ${{ github.event.workflow_run.head_sha }}
|
|
||||||
token: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
|
||||||
site_id: ${{ vars.NETLIFY_SITE_ID }}
|
|
||||||
desc: Shared Component Visual Diffs
|
|
||||||
deployment_env: SharedComponentDiffs
|
|
||||||
prefix: "diffs-"
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
name: Shared Component Visual Tests
|
|
||||||
on:
|
|
||||||
pull_request: {}
|
|
||||||
merge_group:
|
|
||||||
types: [checks_requested]
|
|
||||||
push:
|
|
||||||
branches: [develop, master]
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
permissions: {} # No permissions required
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
testStorybook:
|
|
||||||
name: "Run Visual Tests"
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
permissions:
|
|
||||||
actions: read
|
|
||||||
issues: read
|
|
||||||
pull-requests: read
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
repository: element-hq/element-web
|
|
||||||
|
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
|
||||||
with:
|
|
||||||
cache: "yarn"
|
|
||||||
node-version: "lts/*"
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: yarn install --frozen-lockfile
|
|
||||||
|
|
||||||
- name: Get installed Playwright version
|
|
||||||
id: playwright
|
|
||||||
run: echo "version=$(yarn list --pattern @playwright/test --depth=0 --json --non-interactive --no-progress | jq -r '.data.trees[].name')" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Cache playwright binaries
|
|
||||||
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4
|
|
||||||
id: playwright-cache
|
|
||||||
with:
|
|
||||||
path: ~/.cache/ms-playwright
|
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-playwright-${{ steps.playwright.outputs.version }}-onlyshell
|
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
|
||||||
if: steps.playwright-cache.outputs.cache-hit != 'true'
|
|
||||||
run: "yarn playwright install --with-deps --only-shell"
|
|
||||||
|
|
||||||
- name: Build Element Web resources
|
|
||||||
# Needed to prepare language files
|
|
||||||
run: "yarn build:res"
|
|
||||||
|
|
||||||
- name: Build storybook dependencies
|
|
||||||
# When the first test is ran, it will fail because the dependencies are not yet built.
|
|
||||||
# This step is to ensure that the dependencies are built before running the tests.
|
|
||||||
run: "yarn test:storybook:ci"
|
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Run Visual tests
|
|
||||||
run: "yarn test:storybook:ci"
|
|
||||||
|
|
||||||
- name: Upload received images & diffs
|
|
||||||
if: always()
|
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
||||||
with:
|
|
||||||
name: received-images
|
|
||||||
path: playwright/shared-component-received
|
|
||||||
4
.github/workflows/triage-stale.yml
vendored
@@ -15,14 +15,12 @@ jobs:
|
|||||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
|
||||||
with:
|
with:
|
||||||
operations-per-run: 100
|
operations-per-run: 100
|
||||||
|
|
||||||
# Flaky test issue closing
|
# Flaky test issue closing
|
||||||
any-of-issue-labels: "Z-Flaky-Test-Chrome,Z-Flaky-Test-Firefox,Z-Flaky-Test-Webkit"
|
only-issue-labels: "Z-Flaky-Test"
|
||||||
days-before-issue-stale: 14
|
days-before-issue-stale: 14
|
||||||
days-before-issue-close: 0
|
days-before-issue-close: 0
|
||||||
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
close-issue-message: "This flaky test issue has not been updated in 14 days. It is being closed as presumed resolved."
|
||||||
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
exempt-issue-labels: "Z-Flaky-Test-Disabled"
|
||||||
|
|
||||||
# Stale PR closing
|
# Stale PR closing
|
||||||
days-before-pr-stale: 180
|
days-before-pr-stale: 180
|
||||||
days-before-pr-close: 0
|
days-before-pr-close: 0
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -31,6 +31,3 @@ electron/pub
|
|||||||
/index.html
|
/index.html
|
||||||
# version file and tarball created by `npm pack` / `yarn pack`
|
# version file and tarball created by `npm pack` / `yarn pack`
|
||||||
/git-revision.txt
|
/git-revision.txt
|
||||||
|
|
||||||
*storybook.log
|
|
||||||
storybook-static
|
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { create } from "storybook/theming";
|
|
||||||
|
|
||||||
export default create({
|
|
||||||
base: "light",
|
|
||||||
|
|
||||||
// Colors
|
|
||||||
textColor: "#1b1d22",
|
|
||||||
colorSecondary: "#111111",
|
|
||||||
|
|
||||||
// UI
|
|
||||||
appBg: "#ffffff",
|
|
||||||
appContentBg: "#ffffff",
|
|
||||||
|
|
||||||
// Toolbar
|
|
||||||
barBg: "#ffffff",
|
|
||||||
|
|
||||||
brandTitle: "Element Web",
|
|
||||||
brandUrl: "https://github.com/element-hq/element-web",
|
|
||||||
brandImage: "https://element.io/images/logo-ele-secondary.svg",
|
|
||||||
brandTarget: "_self",
|
|
||||||
});
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Addon, types, useGlobals } from "storybook/manager-api";
|
|
||||||
import { WithTooltip, IconButton, TooltipLinkList } from "storybook/internal/components";
|
|
||||||
import React from "react";
|
|
||||||
import { GlobeIcon } from "@storybook/icons";
|
|
||||||
|
|
||||||
// We can't import `shared/i18n.tsx` directly here.
|
|
||||||
// The storybook addon doesn't seem to benefit the vite config of storybook and we can't resolve the alias in i18n.tsx.
|
|
||||||
import json from "../webapp/i18n/languages.json";
|
|
||||||
const languages = Object.keys(json).filter((lang) => lang !== "default");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the title of a language in the user's locale.
|
|
||||||
*/
|
|
||||||
function languageTitle(language: string): string {
|
|
||||||
return new Intl.DisplayNames([language], { type: "language", style: "short" }).of(language) || language;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const languageAddon: Addon = {
|
|
||||||
title: "Language Selector",
|
|
||||||
type: types.TOOL,
|
|
||||||
render: ({ active }) => {
|
|
||||||
const [globals, updateGlobals] = useGlobals();
|
|
||||||
const selectedLanguage = globals.language || "en";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<WithTooltip
|
|
||||||
placement="top"
|
|
||||||
trigger="click"
|
|
||||||
closeOnOutsideClick
|
|
||||||
tooltip={({ onHide }) => {
|
|
||||||
return (
|
|
||||||
<TooltipLinkList
|
|
||||||
links={languages.map((language) => ({
|
|
||||||
id: language,
|
|
||||||
title: languageTitle(language),
|
|
||||||
active: selectedLanguage === language,
|
|
||||||
onClick: async () => {
|
|
||||||
// Update the global state with the selected language
|
|
||||||
updateGlobals({ language });
|
|
||||||
onHide();
|
|
||||||
},
|
|
||||||
}))}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconButton title="Language">
|
|
||||||
<GlobeIcon />
|
|
||||||
{languageTitle(selectedLanguage)}
|
|
||||||
</IconButton>
|
|
||||||
</WithTooltip>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { StorybookConfig } from "@storybook/react-vite";
|
|
||||||
import path from "node:path";
|
|
||||||
import { nodePolyfills } from "vite-plugin-node-polyfills";
|
|
||||||
import { mergeConfig } from "vite";
|
|
||||||
|
|
||||||
const config: StorybookConfig = {
|
|
||||||
stories: ["../src/shared-components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
|
||||||
staticDirs: ["../webapp"],
|
|
||||||
addons: ["@storybook/addon-docs", "@storybook/addon-designs"],
|
|
||||||
framework: "@storybook/react-vite",
|
|
||||||
core: {
|
|
||||||
disableTelemetry: true,
|
|
||||||
},
|
|
||||||
typescript: {
|
|
||||||
reactDocgen: "react-docgen-typescript",
|
|
||||||
},
|
|
||||||
async viteFinal(config) {
|
|
||||||
return mergeConfig(config, {
|
|
||||||
resolve: {
|
|
||||||
alias: {
|
|
||||||
// Alias used by i18n.tsx
|
|
||||||
$webapp: path.resolve("webapp"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// Needed for counterpart to work
|
|
||||||
plugins: [nodePolyfills({ include: ["process", "util"] })],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
export default config;
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { addons } from "storybook/manager-api";
|
|
||||||
import ElementTheme from "./ElementTheme";
|
|
||||||
import { languageAddon } from "./languageAddon";
|
|
||||||
|
|
||||||
addons.setConfig({
|
|
||||||
theme: ElementTheme,
|
|
||||||
});
|
|
||||||
|
|
||||||
addons.register("elementhq/language", () => addons.add("language", languageAddon));
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.docs-story {
|
|
||||||
background: var(--cpd-color-bg-canvas-default);
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import type { ArgTypes, Preview, Decorator } from "@storybook/react-vite";
|
|
||||||
import { addons } from "storybook/preview-api";
|
|
||||||
|
|
||||||
import "../res/css/shared.pcss";
|
|
||||||
import "./preview.css";
|
|
||||||
import React, { useLayoutEffect } from "react";
|
|
||||||
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
|
|
||||||
import { setLanguage } from "../src/shared-components/i18n";
|
|
||||||
|
|
||||||
export const globalTypes = {
|
|
||||||
theme: {
|
|
||||||
name: "Theme",
|
|
||||||
description: "Global theme for components",
|
|
||||||
toolbar: {
|
|
||||||
icon: "circlehollow",
|
|
||||||
title: "Theme",
|
|
||||||
items: [
|
|
||||||
{ title: "System", value: "system", icon: "browser" },
|
|
||||||
{ title: "Light", value: "light", icon: "sun" },
|
|
||||||
{ title: "Light (high contrast)", value: "light-hc", icon: "sun" },
|
|
||||||
{ title: "Dark", value: "dark", icon: "moon" },
|
|
||||||
{ title: "Dark (high contrast)", value: "dark-hc", icon: "moon" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
language: {
|
|
||||||
name: "Language",
|
|
||||||
description: "Global language for components",
|
|
||||||
},
|
|
||||||
initialGlobals: {
|
|
||||||
theme: "system",
|
|
||||||
language: "en",
|
|
||||||
},
|
|
||||||
} satisfies ArgTypes;
|
|
||||||
|
|
||||||
const allThemesClasses = globalTypes.theme.toolbar.items.map(({ value }) => `cpd-theme-${value}`);
|
|
||||||
|
|
||||||
const ThemeSwitcher: React.FC<{
|
|
||||||
theme: string;
|
|
||||||
}> = ({ theme }) => {
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
document.documentElement.classList.remove(...allThemesClasses);
|
|
||||||
if (theme !== "system") {
|
|
||||||
document.documentElement.classList.add(`cpd-theme-${theme}`);
|
|
||||||
}
|
|
||||||
return () => document.documentElement.classList.remove(...allThemesClasses);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withThemeProvider: Decorator = (Story, context) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ThemeSwitcher theme={context.globals.theme} />
|
|
||||||
<Story />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const LanguageSwitcher: React.FC<{
|
|
||||||
language: string;
|
|
||||||
}> = ({ language }) => {
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const changeLanguage = async (language: string) => {
|
|
||||||
await setLanguage(language);
|
|
||||||
// Force the component to re-render to apply the new language
|
|
||||||
addons.getChannel().emit(FORCE_RE_RENDER);
|
|
||||||
};
|
|
||||||
changeLanguage(language);
|
|
||||||
}, [language]);
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const withLanguageProvider: Decorator = (Story, context) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<LanguageSwitcher language={context.globals.language} />
|
|
||||||
<Story />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const preview: Preview = {
|
|
||||||
tags: ["autodocs"],
|
|
||||||
decorators: [withThemeProvider, withLanguageProvider],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default preview;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2025 New Vector Ltd.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { waitForPageReady } from "@storybook/test-runner";
|
|
||||||
import { toMatchImageSnapshot } from "jest-image-snapshot";
|
|
||||||
|
|
||||||
const customSnapshotsDir = `${process.cwd()}/playwright/shared-component-snapshots/`;
|
|
||||||
const customReceivedDir = `${process.cwd()}/playwright/shared-component-received/`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @type {import('@storybook/test-runner').TestRunnerConfig}
|
|
||||||
*/
|
|
||||||
const config = {
|
|
||||||
setup(page) {
|
|
||||||
expect.extend({ toMatchImageSnapshot });
|
|
||||||
},
|
|
||||||
async postVisit(page, context) {
|
|
||||||
await waitForPageReady(page);
|
|
||||||
|
|
||||||
// If you want to take screenshot of multiple browsers, use
|
|
||||||
// page.context().browser().browserType().name() to get the browser name to prefix the file name
|
|
||||||
const image = await page.screenshot();
|
|
||||||
expect(image).toMatchImageSnapshot({
|
|
||||||
customSnapshotsDir,
|
|
||||||
customSnapshotIdentifier: `${context.id}-${process.platform}`,
|
|
||||||
storeReceivedOnFailure: true,
|
|
||||||
customReceivedDir,
|
|
||||||
customDiffDir: customReceivedDir,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default config;
|
|
||||||
@@ -19,6 +19,3 @@ include:
|
|||||||
|
|
||||||
* Thom Cleary (https://github.com/thomcatdotrocks)
|
* Thom Cleary (https://github.com/thomcatdotrocks)
|
||||||
Small update for tarball deployment
|
Small update for tarball deployment
|
||||||
|
|
||||||
* Alexander (https://github.com/ioalexander)
|
|
||||||
Save image on CTRL + S shortcut
|
|
||||||
|
|||||||
18
CHANGELOG.md
@@ -1,21 +1,3 @@
|
|||||||
Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
|
|
||||||
====================================================================================================
|
|
||||||
## ✨ Features
|
|
||||||
|
|
||||||
* [Backport staging] Fix e2e icon colour ([#30304](https://github.com/element-hq/element-web/pull/30304)). Contributed by @RiotRobot.
|
|
||||||
* Add support for module message hint `allowDownloadingMedia` ([#30252](https://github.com/element-hq/element-web/pull/30252)). Contributed by @Half-Shot.
|
|
||||||
* Update the mobile\_guide page to the new design and link out to Element X by default. ([#30172](https://github.com/element-hq/element-web/pull/30172)). Contributed by @pixlwave.
|
|
||||||
* Filter settings exported when rageshaking ([#30236](https://github.com/element-hq/element-web/pull/30236)). Contributed by @Half-Shot.
|
|
||||||
* Allow Element Call to learn the room name ([#30213](https://github.com/element-hq/element-web/pull/30213)). Contributed by @robintown.
|
|
||||||
|
|
||||||
## 🐛 Bug Fixes
|
|
||||||
|
|
||||||
* [Backport staging] Fix missing image download button ([#30322](https://github.com/element-hq/element-web/pull/30322)). Contributed by @RiotRobot.
|
|
||||||
* Fix transparent verification checkmark in dark mode ([#30235](https://github.com/element-hq/element-web/pull/30235)). Contributed by @Banbuii.
|
|
||||||
* Fix logic in DeviceListener ([#30230](https://github.com/element-hq/element-web/pull/30230)). Contributed by @uhoreg.
|
|
||||||
* Disable file drag-and-drop if insufficient permissions ([#30186](https://github.com/element-hq/element-web/pull/30186)). Contributed by @t3chguy.
|
|
||||||
|
|
||||||
|
|
||||||
Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
|
Changes in [1.11.105](https://github.com/element-hq/element-web/releases/tag/v1.11.105) (2025-07-01)
|
||||||
====================================================================================================
|
====================================================================================================
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|||||||
8
declaration.d.ts
vendored
@@ -1,8 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare module "*.module.css";
|
|
||||||
@@ -17,7 +17,7 @@ const config: Config = {
|
|||||||
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
// This is needed to be able to load dual CJS/ESM WASM packages e.g. rust crypto & matrix-wywiwyg
|
||||||
customExportConditions: ["browser", "node"],
|
customExportConditions: ["browser", "node"],
|
||||||
},
|
},
|
||||||
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/src/shared-components/**/*.test.[t]s?(x)"],
|
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
|
||||||
globalSetup: "<rootDir>/test/globalSetup.ts",
|
globalSetup: "<rootDir>/test/globalSetup.ts",
|
||||||
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||||
|
|||||||
20
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "element-web",
|
"name": "element-web",
|
||||||
"version": "1.11.106",
|
"version": "1.11.105",
|
||||||
"description": "Element: the future of secure communication",
|
"description": "Element: the future of secure communication",
|
||||||
"author": "New Vector Ltd.",
|
"author": "New Vector Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -65,11 +65,7 @@
|
|||||||
"coverage": "yarn test --coverage",
|
"coverage": "yarn test --coverage",
|
||||||
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
"analyse:webpack-bundles": "webpack-bundle-analyzer webpack-stats.json webapp",
|
||||||
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
"update:jitsi": "curl -s https://meet.element.io/libs/external_api.min.js > ./res/jitsi_external_api.min.js",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package"
|
||||||
"storybook": "storybook dev -p 6007",
|
|
||||||
"build-storybook": "storybook build",
|
|
||||||
"test:storybook": "test-storybook --url http://localhost:6007/",
|
|
||||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\""
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"**/pretty-format/react-is": "19.1.0",
|
"**/pretty-format/react-is": "19.1.0",
|
||||||
@@ -97,7 +93,7 @@
|
|||||||
"@types/png-chunks-extract": "^1.0.2",
|
"@types/png-chunks-extract": "^1.0.2",
|
||||||
"@types/react-virtualized": "^9.21.30",
|
"@types/react-virtualized": "^9.21.30",
|
||||||
"@vector-im/compound-design-tokens": "^5.0.0",
|
"@vector-im/compound-design-tokens": "^5.0.0",
|
||||||
"@vector-im/compound-web": "^8.1.2",
|
"@vector-im/compound-web": "^8.2.0",
|
||||||
"@vector-im/matrix-wysiwyg": "2.38.4",
|
"@vector-im/matrix-wysiwyg": "2.38.4",
|
||||||
"@zxcvbn-ts/core": "^3.0.4",
|
"@zxcvbn-ts/core": "^3.0.4",
|
||||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||||
@@ -191,11 +187,6 @@
|
|||||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||||
"@rrweb/types": "^2.0.0-alpha.18",
|
"@rrweb/types": "^2.0.0-alpha.18",
|
||||||
"@sentry/webpack-plugin": "^3.0.0",
|
"@sentry/webpack-plugin": "^3.0.0",
|
||||||
"@storybook/addon-designs": "^10.0.1",
|
|
||||||
"@storybook/addon-docs": "^9.0.12",
|
|
||||||
"@storybook/icons": "^1.4.0",
|
|
||||||
"@storybook/react-vite": "^9.0.15",
|
|
||||||
"@storybook/test-runner": "^0.23.0",
|
|
||||||
"@stylistic/eslint-plugin": "^5.0.0",
|
"@stylistic/eslint-plugin": "^5.0.0",
|
||||||
"@svgr/webpack": "^8.0.0",
|
"@svgr/webpack": "^8.0.0",
|
||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
@@ -255,7 +246,6 @@
|
|||||||
"eslint-plugin-react": "^7.28.0",
|
"eslint-plugin-react": "^7.28.0",
|
||||||
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
"eslint-plugin-react-compiler": "^19.0.0-beta-df7b47d-20241124",
|
||||||
"eslint-plugin-react-hooks": "^5.0.0",
|
"eslint-plugin-react-hooks": "^5.0.0",
|
||||||
"eslint-plugin-storybook": "^9.0.12",
|
|
||||||
"eslint-plugin-unicorn": "^56.0.0",
|
"eslint-plugin-unicorn": "^56.0.0",
|
||||||
"express": "^5.0.0",
|
"express": "^5.0.0",
|
||||||
"fake-indexeddb": "^6.0.0",
|
"fake-indexeddb": "^6.0.0",
|
||||||
@@ -267,7 +257,6 @@
|
|||||||
"jest": "^29.6.2",
|
"jest": "^29.6.2",
|
||||||
"jest-canvas-mock": "^2.5.2",
|
"jest-canvas-mock": "^2.5.2",
|
||||||
"jest-environment-jsdom": "^29.7.0",
|
"jest-environment-jsdom": "^29.7.0",
|
||||||
"jest-image-snapshot": "^6.5.1",
|
|
||||||
"jest-mock": "^29.6.2",
|
"jest-mock": "^29.6.2",
|
||||||
"jest-raw-loader": "^1.0.1",
|
"jest-raw-loader": "^1.0.1",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
@@ -296,7 +285,6 @@
|
|||||||
"rimraf": "^6.0.0",
|
"rimraf": "^6.0.0",
|
||||||
"semver": "^7.5.2",
|
"semver": "^7.5.2",
|
||||||
"source-map-loader": "^5.0.0",
|
"source-map-loader": "^5.0.0",
|
||||||
"storybook": "^9.0.12",
|
|
||||||
"stylelint": "^16.13.0",
|
"stylelint": "^16.13.0",
|
||||||
"stylelint-config-standard": "^38.0.0",
|
"stylelint-config-standard": "^38.0.0",
|
||||||
"stylelint-scss": "^6.0.0",
|
"stylelint-scss": "^6.0.0",
|
||||||
@@ -306,8 +294,6 @@
|
|||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"util": "^0.12.5",
|
"util": "^0.12.5",
|
||||||
"vite": "^7.0.1",
|
|
||||||
"vite-plugin-node-polyfills": "^0.24.0",
|
|
||||||
"web-streams-polyfill": "^4.0.0",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.89.0",
|
"webpack": "^5.89.0",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
diff --git a/node_modules/@types/mdx/types.d.ts b/node_modules/@types/mdx/types.d.ts
|
|
||||||
index 498bb69..4e89216 100644
|
|
||||||
--- a/node_modules/@types/mdx/types.d.ts
|
|
||||||
+++ b/node_modules/@types/mdx/types.d.ts
|
|
||||||
@@ -5,7 +5,7 @@
|
|
||||||
*/
|
|
||||||
// @ts-ignore JSX runtimes may optionally define JSX.ElementType. The MDX types need to work regardless whether this is
|
|
||||||
// defined or not.
|
|
||||||
-type ElementType = any extends JSX.ElementType ? never : JSX.ElementType;
|
|
||||||
+type ElementType = any extends JSX.ElementType ? never : React.JSX.ElementType;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This matches any function component types that ar part of `ElementType`.
|
|
||||||
@@ -20,12 +20,12 @@ type ClassElementType = Extract<ElementType, new(props: Record<string, any>) =>
|
|
||||||
/**
|
|
||||||
* A valid JSX string component.
|
|
||||||
*/
|
|
||||||
-type StringComponent = Extract<keyof JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
|
|
||||||
+type StringComponent = Extract<keyof React.JSX.IntrinsicElements, ElementType extends never ? string : ElementType>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A JSX element returned by MDX content.
|
|
||||||
*/
|
|
||||||
-export type Element = JSX.Element;
|
|
||||||
+export type Element = React.JSX.Element;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A valid JSX function component.
|
|
||||||
@@ -44,7 +44,7 @@ type FunctionComponent<Props> = ElementType extends never
|
|
||||||
*/
|
|
||||||
type ClassComponent<Props> = ElementType extends never
|
|
||||||
// If JSX.ElementType isn’t defined, the valid return type is a constructor that returns JSX.ElementClass
|
|
||||||
- ? new(props: Props) => JSX.ElementClass
|
|
||||||
+ ? new(props: Props) => React.JSX.ElementClass
|
|
||||||
: ClassElementType extends never
|
|
||||||
// If JSX.ElementType is defined, but doesn’t allow constructors, function components are disallowed.
|
|
||||||
? never
|
|
||||||
@@ -70,7 +70,7 @@ interface NestedMDXComponents {
|
|
||||||
export type MDXComponents =
|
|
||||||
& NestedMDXComponents
|
|
||||||
& {
|
|
||||||
- [Key in StringComponent]?: Component<JSX.IntrinsicElements[Key]>;
|
|
||||||
+ [Key in StringComponent]?: Component<React.JSX.IntrinsicElements[Key]>;
|
|
||||||
}
|
|
||||||
& {
|
|
||||||
/**
|
|
||||||
@@ -1,34 +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 OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
|
||||||
|
|
||||||
test.describe("Create Room", () => {
|
|
||||||
test.use({ displayName: "Jim" });
|
|
||||||
|
|
||||||
test("should allow us to create a public room with name, topic & address set", async ({ page, user, app }) => {
|
|
||||||
const name = "Test room 1";
|
|
||||||
const topic = "This room is dedicated to this test and this test only!";
|
|
||||||
|
|
||||||
const dialog = await app.openCreateRoomDialog();
|
|
||||||
// Fill name & topic
|
|
||||||
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
|
||||||
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
|
||||||
// Change room to public
|
|
||||||
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
|
||||||
await dialog.getByRole("option", { name: "Public room" }).click();
|
|
||||||
// Fill room address
|
|
||||||
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
|
|
||||||
// Submit
|
|
||||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
|
||||||
|
|
||||||
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
|
||||||
const header = page.locator(".mx_RoomHeader");
|
|
||||||
await expect(header).toContainText(name);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -168,7 +168,6 @@ test.describe("Cryptography", function () {
|
|||||||
|
|
||||||
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
// Take a snapshot of RoomSummaryCard with a verified E2EE icon
|
||||||
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
await expect(page.locator(".mx_RightPanel")).toMatchScreenshot("RoomSummaryCard-with-verified-e2ee.png");
|
||||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon.png");
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -48,38 +48,31 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||||||
return promiseVerificationRequest;
|
return promiseVerificationRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
test(
|
test("Verify device with SAS during login", async ({ page, app, credentials, homeserver }) => {
|
||||||
"Verify device with SAS during login",
|
await logIntoElement(page, credentials);
|
||||||
{ tag: "@screenshot" },
|
|
||||||
async ({ page, app, credentials, homeserver }) => {
|
|
||||||
await logIntoElement(page, credentials);
|
|
||||||
|
|
||||||
// Launch the verification request between alice and the bot
|
// Launch the verification request between alice and the bot
|
||||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||||
|
|
||||||
// Handle emoji SAS verification
|
// Handle emoji SAS verification
|
||||||
const infoDialog = page.locator(".mx_InfoDialog");
|
const infoDialog = page.locator(".mx_InfoDialog");
|
||||||
// the bot chooses to do an emoji verification
|
// the bot chooses to do an emoji verification
|
||||||
const verifier = await verificationRequest.evaluateHandle((request) =>
|
const verifier = await verificationRequest.evaluateHandle((request) => request.startVerification("m.sas.v1"));
|
||||||
request.startVerification("m.sas.v1"),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Handle emoji request and check that emojis are matching
|
// Handle emoji request and check that emojis are matching
|
||||||
await doTwoWaySasVerification(page, verifier);
|
await doTwoWaySasVerification(page, verifier);
|
||||||
|
|
||||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||||
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
|
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
|
||||||
|
|
||||||
// Check that our device is now cross-signed
|
// Check that our device is now cross-signed
|
||||||
await checkDeviceIsCrossSigned(app);
|
await checkDeviceIsCrossSigned(app);
|
||||||
|
|
||||||
// Check that the current device is connected to key backup
|
// Check that the current device is connected to key backup
|
||||||
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
// For now we don't check that the backup key is in cache because it's a bit flaky,
|
||||||
// as we need to wait for the secret gossiping to happen.
|
// as we need to wait for the secret gossiping to happen.
|
||||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, false);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// Regression test for https://github.com/element-hq/element-web/issues/29110
|
// Regression test for https://github.com/element-hq/element-web/issues/29110
|
||||||
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
|
test("No toast after verification, even if the secrets take a while to arrive", async ({ page, credentials }) => {
|
||||||
|
|||||||
27
playwright/e2e/devtools/devtools.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Devtools", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the devtools", { tag: "@screenshot" }, async ({ page, homeserver, user, app }) => {
|
||||||
|
await app.client.createRoom({ name: "Test Room" });
|
||||||
|
await app.viewRoomByName("Test Room");
|
||||||
|
|
||||||
|
const composer = app.getComposer().locator("[contenteditable]");
|
||||||
|
await composer.fill("/devtools");
|
||||||
|
await composer.press("Enter");
|
||||||
|
const dialog = page.locator(".mx_Dialog");
|
||||||
|
await dialog.getByLabel("Developer mode").check();
|
||||||
|
|
||||||
|
await expect(dialog).toMatchScreenshot("devtools-dialog.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
31
playwright/e2e/devtools/upgraderoom.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Room upgrade dialog", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should render the room upgrade dialog", { tag: "@screenshot" }, async ({ page, homeserver, user, app }) => {
|
||||||
|
// Enable developer mode
|
||||||
|
await app.settings.setValue("developerMode", null, SettingLevel.ACCOUNT, true);
|
||||||
|
|
||||||
|
await app.client.createRoom({ name: "Test Room" });
|
||||||
|
await app.viewRoomByName("Test Room");
|
||||||
|
|
||||||
|
const composer = app.getComposer().locator("[contenteditable]");
|
||||||
|
// Pick a room version that is likely to be supported by all our target homeservers.
|
||||||
|
await composer.fill("/upgraderoom 5");
|
||||||
|
await composer.press("Enter");
|
||||||
|
const dialog = page.locator(".mx_Dialog");
|
||||||
|
await dialog.getByLabel("Automatically invite members from this room to the new one").check();
|
||||||
|
await expect(dialog).toMatchScreenshot("upgrade-room.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Decline and block invite dialog", function () {
|
||||||
|
test.use({
|
||||||
|
displayName: "Hanako",
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should show decline and block dialog for a room",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user, bot }) => {
|
||||||
|
await bot.createRoom({ name: "Test Room", invite: [user.userId] });
|
||||||
|
await app.viewRoomByName("Test Room");
|
||||||
|
await page.getByRole("button", { name: "Decline and block" }).click();
|
||||||
|
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("decline-and-block-invite-empty.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -49,7 +49,8 @@ test.describe("Room list", () => {
|
|||||||
// Put focus on the room list
|
// Put focus on the room list
|
||||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
// Scroll to the end of the room list
|
// Scroll to the end of the room list
|
||||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
await page.mouse.wheel(0, 1000);
|
||||||
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||||
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
await expect(roomListView).toMatchScreenshot("room-list-scrolled.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,8 +120,10 @@ test.describe("Room list", () => {
|
|||||||
// Put focus on the room list
|
// Put focus on the room list
|
||||||
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room28" }).click();
|
||||||
|
|
||||||
// Scroll to the end of the room list
|
while (!(await roomItem.isVisible())) {
|
||||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
// Scroll to the end of the room list
|
||||||
|
await page.mouse.wheel(0, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
// The room decoration should have the muted icon
|
// The room decoration should have the muted icon
|
||||||
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
await expect(roomItem.getByTestId("notification-decoration")).toBeVisible();
|
||||||
@@ -141,7 +144,7 @@ test.describe("Room list", () => {
|
|||||||
// Put focus on the room list
|
// Put focus on the room list
|
||||||
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room29" }).click();
|
||||||
// Scroll to the end of the room list
|
// Scroll to the end of the room list
|
||||||
await app.scrollListToBottom(page.locator(".mx_RoomList_List"));
|
await page.mouse.wheel(0, 1000);
|
||||||
|
|
||||||
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
await expect(roomListView.getByRole("gridcell", { name: "Open room room0" })).toBeVisible();
|
||||||
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
await roomListView.getByRole("gridcell", { name: "Open room room0" }).click();
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
Copyright 2024 New Vector Ltd.
|
Copyright 2024, 2025 New Vector Ltd.
|
||||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
@@ -57,4 +57,20 @@ test.describe("Location sharing", { tag: "@no-firefox" }, () => {
|
|||||||
|
|
||||||
await expect(page.locator(".mx_Marker")).toBeVisible();
|
await expect(page.locator(".mx_Marker")).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"is prompted for and can consent to live location sharing",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, user, app }) => {
|
||||||
|
await app.viewRoomById(await app.client.createRoom({}));
|
||||||
|
|
||||||
|
const composerOptions = await app.openMessageComposerOptions();
|
||||||
|
await composerOptions.getByRole("menuitem", { name: "Location", exact: true }).click();
|
||||||
|
const menu = page.locator(".mx_LocationShareMenu");
|
||||||
|
|
||||||
|
await menu.getByRole("button", { name: "My live location" }).click();
|
||||||
|
await menu.getByLabel("Enable live location sharing").check();
|
||||||
|
await expect(menu).toMatchScreenshot("location-live-share-dialog.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
64
playwright/e2e/room/create-room.spec.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
const name = "Test room";
|
||||||
|
const topic = "A decently explanatory topic for a test room.";
|
||||||
|
|
||||||
|
test.describe("Create Room", () => {
|
||||||
|
test.use({ displayName: "Jim" });
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should create a public room with name, topic & address set",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, user, app }) => {
|
||||||
|
const dialog = await app.openCreateRoomDialog();
|
||||||
|
// Fill name & topic
|
||||||
|
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
||||||
|
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
||||||
|
// Change room to public
|
||||||
|
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
||||||
|
await dialog.getByRole("option", { name: "Public room" }).click();
|
||||||
|
// Fill room address
|
||||||
|
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
|
||||||
|
// Snapshot it
|
||||||
|
await expect(dialog).toMatchScreenshot("create-room.png");
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||||
|
const header = page.locator(".mx_RoomHeader");
|
||||||
|
await expect(header).toContainText(name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test("should create a video room", { tag: "@screenshot" }, async ({ page, user, app }) => {
|
||||||
|
await app.settings.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
||||||
|
|
||||||
|
const dialog = await app.openCreateRoomDialog("New video room");
|
||||||
|
// Fill name & topic
|
||||||
|
await dialog.getByRole("textbox", { name: "Name" }).fill(name);
|
||||||
|
await dialog.getByRole("textbox", { name: "Topic" }).fill(topic);
|
||||||
|
// Change room to public
|
||||||
|
await dialog.getByRole("button", { name: "Room visibility" }).click();
|
||||||
|
await dialog.getByRole("option", { name: "Public room" }).click();
|
||||||
|
// Fill room address
|
||||||
|
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-room-1");
|
||||||
|
// Snapshot it
|
||||||
|
await expect(dialog).toMatchScreenshot("create-video-room.png");
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
await dialog.getByRole("button", { name: "Create video room" }).click();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(`/#/room/#test-room-1:${user.homeServer}`));
|
||||||
|
const header = page.locator(".mx_RoomHeader");
|
||||||
|
await expect(header).toContainText(name);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -50,8 +50,8 @@ test.describe("Appearance user settings tab", () => {
|
|||||||
// Click "Show advanced" link button
|
// Click "Show advanced" link button
|
||||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||||
|
|
||||||
await tab.getByLabel("Use bundled emoji font").click();
|
await tab.getByRole("switch", { name: "Use bundled emoji font" }).click();
|
||||||
await tab.getByLabel("Use a system font").click();
|
await tab.getByRole("switch", { name: "Use a system font" }).click();
|
||||||
|
|
||||||
// Assert that the font-family value was removed
|
// Assert that the font-family value was removed
|
||||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../../element-web-test";
|
||||||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
|
test.describe("Notifications 2 tab", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
await app.settings.setValue("feature_notification_settings2", null, SettingLevel.DEVICE, true);
|
||||||
|
await page.setViewportSize({ width: 1024, height: 2000 });
|
||||||
|
const settings = await app.settings.openUserSettings("Notifications");
|
||||||
|
await expect(settings).toMatchScreenshot("standard-notifications-2-settings.png", {
|
||||||
|
// Mask the mxid.
|
||||||
|
mask: [settings.locator("#mx_NotificationSettings2_MentionCheckbox span")],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Notifications tab", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should display notification settings", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||||
|
const settings = await app.settings.openUserSettings("Notifications");
|
||||||
|
await settings.getByLabel("Enable notifications for this account").check();
|
||||||
|
await settings.getByLabel("Enable notifications for this session").check();
|
||||||
|
await expect(settings).toMatchScreenshot("standard-notification-settings.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import { type Locator } from "@playwright/test";
|
import { type Locator } from "@playwright/test";
|
||||||
|
|
||||||
import { test, expect } from "../../element-web-test";
|
import { test, expect } from "../../../element-web-test";
|
||||||
|
|
||||||
test.describe("Roles & Permissions room settings tab", () => {
|
test.describe("Roles & Permissions room settings tab", () => {
|
||||||
const roomName = "Test room";
|
const roomName = "Test room";
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Locator } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test, expect } from "../../../element-web-test";
|
||||||
|
|
||||||
|
test.describe("Roles & Permissions room settings tab", () => {
|
||||||
|
const roomName = "Test room";
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
let settings: Locator;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ user, app }) => {
|
||||||
|
await app.client.createRoom({ name: roomName });
|
||||||
|
await app.viewRoomByName(roomName);
|
||||||
|
settings = await app.settings.openRoomSettings("Security & Privacy");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should be able to toggle on encryption in a room", { tag: "@screenshot" }, async ({ page, app, user }) => {
|
||||||
|
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||||
|
const encryptedToggle = settings.getByLabel("Encrypted");
|
||||||
|
await encryptedToggle.click();
|
||||||
|
|
||||||
|
// Accept the dialog.
|
||||||
|
await page.getByRole("button", { name: "Ok " }).click();
|
||||||
|
|
||||||
|
await expect(encryptedToggle).toBeChecked();
|
||||||
|
await expect(encryptedToggle).toBeDisabled();
|
||||||
|
|
||||||
|
await settings.getByLabel("Only send messages to verified users.").check();
|
||||||
|
await expect(settings).toMatchScreenshot("room-security-settings.png");
|
||||||
|
});
|
||||||
|
});
|
||||||
40
playwright/e2e/settings/room-settings/room-video-tab.spec.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 New Vector Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type Locator } from "@playwright/test";
|
||||||
|
|
||||||
|
import { test, expect } from "../../../element-web-test";
|
||||||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
|
test.describe("Voice & Video room settings tab", () => {
|
||||||
|
const roomName = "Test room";
|
||||||
|
|
||||||
|
test.use({
|
||||||
|
displayName: "Alice",
|
||||||
|
});
|
||||||
|
|
||||||
|
let settings: Locator;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ user, app, page }) => {
|
||||||
|
// Execute client actions before setting, as the setting will force a reload.
|
||||||
|
await app.client.createRoom({ name: roomName });
|
||||||
|
await app.settings.setValue("feature_group_calls", null, SettingLevel.DEVICE, true);
|
||||||
|
await app.viewRoomByName(roomName);
|
||||||
|
settings = await app.settings.openRoomSettings("Voice & Video");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should be able to toggle on Element Call in the room",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||||
|
const callToggle = settings.getByLabel("Enable Element Call as an additional calling option in this room");
|
||||||
|
await callToggle.check();
|
||||||
|
await expect(settings).toMatchScreenshot("room-video-settings.png");
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -41,6 +41,18 @@ test.describe("Security user settings tab", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should render the security tab", { tag: "@screenshot" }, async ({ app, page, user }) => {
|
||||||
|
await page.setViewportSize({ width: 1024, height: 1400 });
|
||||||
|
const tab = await app.settings.openUserSettings("Security");
|
||||||
|
await expect(tab).toMatchScreenshot("security-settings-tab.png", {
|
||||||
|
mask: [
|
||||||
|
// Contains IM name.
|
||||||
|
tab.locator("#mx_SetIntegrationManager_BodyText"),
|
||||||
|
tab.locator("#mx_SetIntegrationManager_ManagerName"),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("should be able to set an ID server", async ({ app, context, user, page }) => {
|
test("should be able to set an ID server", async ({ app, context, user, page }) => {
|
||||||
const tab = await app.settings.openUserSettings("Security");
|
const tab = await app.settings.openUserSettings("Security");
|
||||||
|
|
||||||
|
|||||||
@@ -369,4 +369,16 @@ test.describe("Spaces", () => {
|
|||||||
await app.viewSpaceByName("Root Space");
|
await app.viewSpaceByName("Root Space");
|
||||||
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
await expect(page.locator(".mx_SpaceRoomView")).toMatchScreenshot("space-room-view.png");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should render spaces visibility settings", { tag: "@screenshot" }, async ({ page, app, user, bot }) => {
|
||||||
|
await app.client.createSpace({
|
||||||
|
name: "My Space",
|
||||||
|
});
|
||||||
|
await app.viewSpaceByName("My space");
|
||||||
|
await page.getByLabel("Settings", { exact: true }).click();
|
||||||
|
await app.settings.switchTab("Visibility");
|
||||||
|
await expect(page.locator("#mx_tabpanel_SPACE_VISIBILITY_TAB")).toMatchScreenshot(
|
||||||
|
"space-visibility-settings.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
95
playwright/e2e/widgets/permissions-dialog.spec.ts
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from "../../element-web-test";
|
||||||
|
|
||||||
|
const DEMO_WIDGET_ID = "demo-widget-id";
|
||||||
|
const DEMO_WIDGET_NAME = "Demo Widget";
|
||||||
|
const DEMO_WIDGET_TYPE = "demo";
|
||||||
|
const ROOM_NAME = "Demo";
|
||||||
|
|
||||||
|
const DEMO_WIDGET_HTML = `
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Demo Widget</title>
|
||||||
|
<script>
|
||||||
|
let sendEventCount = 0
|
||||||
|
window.onmessage = ev => {
|
||||||
|
if (ev.data.action === 'capabilities') {
|
||||||
|
window.parent.postMessage(Object.assign({
|
||||||
|
response: {
|
||||||
|
capabilities: [
|
||||||
|
"org.matrix.msc2762.timeline:*",
|
||||||
|
"org.matrix.msc2762.receive.state_event:m.room.topic",
|
||||||
|
"org.matrix.msc2762.send.event:net.widget_echo"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}, ev.data), '*');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
test.describe("Widger permissions dialog", () => {
|
||||||
|
test.use({
|
||||||
|
displayName: "Mike",
|
||||||
|
});
|
||||||
|
|
||||||
|
let demoWidgetUrl: string;
|
||||||
|
test.beforeEach(async ({ webserver }) => {
|
||||||
|
demoWidgetUrl = webserver.start(DEMO_WIDGET_HTML);
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"should be updated if user is re-invited into the room with updated state event",
|
||||||
|
{ tag: "@screenshot" },
|
||||||
|
async ({ page, app, user }) => {
|
||||||
|
const roomId = await app.client.createRoom({
|
||||||
|
name: ROOM_NAME,
|
||||||
|
});
|
||||||
|
|
||||||
|
// setup widget via state event
|
||||||
|
await app.client.sendStateEvent(
|
||||||
|
roomId,
|
||||||
|
"im.vector.modular.widgets",
|
||||||
|
{
|
||||||
|
id: DEMO_WIDGET_ID,
|
||||||
|
creatorUserId: "somebody",
|
||||||
|
type: DEMO_WIDGET_TYPE,
|
||||||
|
name: DEMO_WIDGET_NAME,
|
||||||
|
url: demoWidgetUrl,
|
||||||
|
},
|
||||||
|
DEMO_WIDGET_ID,
|
||||||
|
);
|
||||||
|
|
||||||
|
// set initial layout
|
||||||
|
await app.client.sendStateEvent(
|
||||||
|
roomId,
|
||||||
|
"io.element.widgets.layout",
|
||||||
|
{
|
||||||
|
widgets: {
|
||||||
|
[DEMO_WIDGET_ID]: {
|
||||||
|
container: "top",
|
||||||
|
index: 1,
|
||||||
|
width: 100,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
// open the room
|
||||||
|
await app.viewRoomByName(ROOM_NAME);
|
||||||
|
await expect(page.locator(".mx_WidgetCapabilitiesPromptDialog")).toMatchScreenshot(
|
||||||
|
"widget-capabilites-prompt.png",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -51,9 +51,9 @@ export class ElementAppPage {
|
|||||||
/**
|
/**
|
||||||
* Open room creation dialog.
|
* Open room creation dialog.
|
||||||
*/
|
*/
|
||||||
public async openCreateRoomDialog(): Promise<Locator> {
|
public async openCreateRoomDialog(roomKindname: "New room" | "New video room" = "New room"): Promise<Locator> {
|
||||||
await this.page.getByRole("button", { name: "Add room", exact: true }).click();
|
await this.page.getByRole("button", { name: "Add room", exact: true }).click();
|
||||||
await this.page.getByRole("menuitem", { name: "New room", exact: true }).click();
|
await this.page.getByRole("menuitem", { name: roomKindname, exact: true }).click();
|
||||||
return this.page.locator(".mx_CreateRoomDialog");
|
return this.page.locator(".mx_CreateRoomDialog");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,26 +213,4 @@ export class ElementAppPage {
|
|||||||
.getByRole("button", { name: "Dismiss" })
|
.getByRole("button", { name: "Dismiss" })
|
||||||
.click();
|
.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Scroll an infinite list to the bottom.
|
|
||||||
* @param list The element to scroll
|
|
||||||
*/
|
|
||||||
public async scrollListToBottom(list: Locator): Promise<void> {
|
|
||||||
// First hover the mouse over the element that we want to scroll
|
|
||||||
await list.hover();
|
|
||||||
|
|
||||||
const needsScroll = async () => {
|
|
||||||
// From https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled
|
|
||||||
const fullyScrolled = await list.evaluate(
|
|
||||||
(e) => Math.abs(e.scrollHeight - e.clientHeight - e.scrollTop) <= 1,
|
|
||||||
);
|
|
||||||
return !fullyScrolled;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scroll the element until we detect that it is fully scrolled
|
|
||||||
do {
|
|
||||||
await this.page.mouse.wheel(0, 1000);
|
|
||||||
} while (await needsScroll());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class Settings {
|
|||||||
* @param {*} value The new value of the setting, may be null.
|
* @param {*} value The new value of the setting, may be null.
|
||||||
* @return {Promise} Resolves when the setting has been changed.
|
* @return {Promise} Resolves when the setting has been changed.
|
||||||
*/
|
*/
|
||||||
public async setValue(settingName: string, roomId: string, level: SettingLevel, value: any): Promise<void> {
|
public async setValue(settingName: string, roomId: string | null, level: SettingLevel, value: any): Promise<void> {
|
||||||
return this.page.evaluate<
|
return this.page.evaluate<
|
||||||
Promise<void>,
|
Promise<void>,
|
||||||
{
|
{
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.9 KiB |
|
Before Width: | Height: | Size: 367 B |
|
Before Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 179 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
Before Width: | Height: | Size: 272 KiB After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 40 KiB |
|
After Width: | Height: | Size: 32 KiB |
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||||
|
|
||||||
const TAG = "develop@sha256:b38e55f06543f83f5a13f1d843489eb7aeaf7370a5c17a51897b462eeca315f5";
|
const TAG = "develop@sha256:aea1d8f371268aed7a5863fa5dde960fb4f9f578cd0a5952cc4da92537f95cfa";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2025 New Vector Ltd.
|
|
||||||
*
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
* Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css") layer(compound);
|
|
||||||
@import url("@vector-im/compound-web/dist/style.css");
|
|
||||||
@@ -28,6 +28,12 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
--collapsedWidth: 68px;
|
--collapsedWidth: 68px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanel_newRoomList {
|
||||||
|
/* Thew new rooms list is not designed to be collapsed to just icons. */
|
||||||
|
/* 224 + 68(spaces bar) was deemed by design to be a good minimum for the left panel. */
|
||||||
|
--collapsedWidth: 224px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_wrapper {
|
.mx_LeftPanel_wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
@@ -240,11 +246,3 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_newRoomList {
|
|
||||||
/* Thew new rooms list is not designed to be collapsed to just icons. */
|
|
||||||
/* 224 + 68(spaces bar) was deemed by design to be a good minimum for the left panel. */
|
|
||||||
--collapsedWidth: 224px;
|
|
||||||
/* Important to force the color on ED titlebar until we remove the old room list */
|
|
||||||
background-color: var(--cpd-color-bg-canvas-default) !important;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -52,10 +52,17 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
.mx_E2EIcon_normal::after {
|
.mx_E2EIcon_normal::after {
|
||||||
mask-image: url("$(res)/img/e2e/normal.svg");
|
mask-image: url("$(res)/img/e2e/normal.svg");
|
||||||
background-color: white;
|
background-color: var(--cpd-color-icon-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_E2EIcon_verified::after {
|
.mx_E2EIcon_verified::after {
|
||||||
mask-image: url("$(res)/img/e2e/verified.svg");
|
mask-image: url("$(res)/img/e2e/verified.svg");
|
||||||
background-color: $e2e-verified-color;
|
background-color: $e2e-verified-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When using the "normal" icon as a background for verified or warning icon,
|
||||||
|
// it should be slightly smaller than the foreground icon
|
||||||
|
.mx_E2EIcon_verified, .mx_E2EIcon_warning .mx_E2EIcon_normal::after {
|
||||||
|
mask-size: 90%;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|||||||
@@ -77,10 +77,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SettingsTab_toggleWithDescription {
|
|
||||||
margin-top: $spacing-24;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SettingsTab_sections {
|
.mx_SettingsTab_sections {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
.mx_Field.mx_AppearanceUserSettingsTab_checkboxControlledField {
|
.mx_Field.mx_AppearanceUserSettingsTab_checkboxControlledField {
|
||||||
width: 256px;
|
width: 256px;
|
||||||
/* matches checkbox box + padding to align with checkbox label */
|
/* Line up with Settings field toggle button */
|
||||||
margin-inline-start: calc($font-16px + 10px);
|
margin-inline-start: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ sonar.organization=element-hq
|
|||||||
#sonar.sourceEncoding=UTF-8
|
#sonar.sourceEncoding=UTF-8
|
||||||
|
|
||||||
sonar.sources=src,res
|
sonar.sources=src,res
|
||||||
sonar.tests=test,playwright,src
|
sonar.tests=test,playwright
|
||||||
sonar.test.inclusions=test/*,playwright/*,src/**/*.test.tsx
|
|
||||||
sonar.exclusions=__mocks__,docs,element.io,nginx
|
sonar.exclusions=__mocks__,docs,element.io,nginx
|
||||||
|
|
||||||
sonar.cpd.exclusions=src/i18n/strings/*.json
|
sonar.cpd.exclusions=src/i18n/strings/*.json
|
||||||
|
|||||||
1
src/@types/global.d.ts
vendored
@@ -135,7 +135,6 @@ declare global {
|
|||||||
initialise(): Promise<{
|
initialise(): Promise<{
|
||||||
protocol: string;
|
protocol: string;
|
||||||
sessionId: string;
|
sessionId: string;
|
||||||
supportsBadgeOverlay: boolean;
|
|
||||||
config: IConfigOptions;
|
config: IConfigOptions;
|
||||||
supportedSettings: Record<string, boolean>;
|
supportedSettings: Record<string, boolean>;
|
||||||
}>;
|
}>;
|
||||||
|
|||||||
@@ -494,12 +494,15 @@ export default abstract class BasePlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateFavicon(): void {
|
private updateFavicon(): void {
|
||||||
const notif: string | number = this.notificationCount;
|
let bgColor = "#d00";
|
||||||
|
let notif: string | number = this.notificationCount;
|
||||||
|
|
||||||
if (this.errorDidOccur) {
|
if (this.errorDidOccur) {
|
||||||
this.favicon.badge(notif || "×", { bgColor: "#f00" });
|
notif = notif || "×";
|
||||||
|
bgColor = "#f00";
|
||||||
}
|
}
|
||||||
this.favicon.badge(notif);
|
|
||||||
|
this.favicon.badge(notif, { bgColor });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
type SyncState,
|
type SyncState,
|
||||||
ClientStoppedError,
|
ClientStoppedError,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger as baseLogger, type BaseLogger, LogSpan } from "matrix-js-sdk/src/logger";
|
import { logger as baseLogger, LogSpan } from "matrix-js-sdk/src/logger";
|
||||||
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
import { CryptoEvent, type KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
|
||||||
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
import { type CryptoSessionStateChange } from "@matrix-org/analytics-events/types/typescript/CryptoSessionStateChange";
|
||||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
@@ -213,7 +213,6 @@ export default class DeviceListener {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private onKeyBackupStatusChanged = (): void => {
|
private onKeyBackupStatusChanged = (): void => {
|
||||||
logger.info("Backup status changed");
|
|
||||||
this.cachedKeyBackupUploadActive = undefined;
|
this.cachedKeyBackupUploadActive = undefined;
|
||||||
this.recheck();
|
this.recheck();
|
||||||
};
|
};
|
||||||
@@ -314,7 +313,6 @@ export default class DeviceListener {
|
|||||||
private async doRecheck(): Promise<void> {
|
private async doRecheck(): Promise<void> {
|
||||||
if (!this.running || !this.client) return; // we have been stopped
|
if (!this.running || !this.client) return; // we have been stopped
|
||||||
const logSpan = new LogSpan(logger, "check_" + secureRandomString(4));
|
const logSpan = new LogSpan(logger, "check_" + secureRandomString(4));
|
||||||
logSpan.debug("starting recheck...");
|
|
||||||
|
|
||||||
const cli = this.client;
|
const cli = this.client;
|
||||||
|
|
||||||
@@ -357,7 +355,7 @@ export default class DeviceListener {
|
|||||||
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
|
(await crypto.getDeviceVerificationStatus(cli.getSafeUserId(), cli.deviceId!))?.crossSigningVerified,
|
||||||
);
|
);
|
||||||
|
|
||||||
const keyBackupUploadActive = await this.isKeyBackupUploadActive(logSpan);
|
const keyBackupUploadActive = await this.isKeyBackupUploadActive();
|
||||||
const backupDisabled = await this.recheckBackupDisabled(cli);
|
const backupDisabled = await this.recheckBackupDisabled(cli);
|
||||||
|
|
||||||
// We warn if key backup upload is turned off and we have not explicitly
|
// We warn if key backup upload is turned off and we have not explicitly
|
||||||
@@ -581,7 +579,7 @@ export default class DeviceListener {
|
|||||||
* trigger an auto-rageshake).
|
* trigger an auto-rageshake).
|
||||||
*/
|
*/
|
||||||
private checkKeyBackupStatus = async (): Promise<void> => {
|
private checkKeyBackupStatus = async (): Promise<void> => {
|
||||||
if (!(await this.isKeyBackupUploadActive(logger))) {
|
if (!(await this.isKeyBackupUploadActive())) {
|
||||||
dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
|
dis.dispatch({ action: Action.ReportKeyBackupNotEnabled });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -589,7 +587,7 @@ export default class DeviceListener {
|
|||||||
/**
|
/**
|
||||||
* Is key backup enabled? Use a cached answer if we have one.
|
* Is key backup enabled? Use a cached answer if we have one.
|
||||||
*/
|
*/
|
||||||
private isKeyBackupUploadActive = async (logger: BaseLogger): Promise<boolean> => {
|
private isKeyBackupUploadActive = async (): Promise<boolean> => {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
// To preserve existing behaviour, if there is no client, we
|
// To preserve existing behaviour, if there is no client, we
|
||||||
// pretend key backup upload is on.
|
// pretend key backup upload is on.
|
||||||
@@ -613,7 +611,6 @@ export default class DeviceListener {
|
|||||||
// Fetch the answer and cache it
|
// Fetch the answer and cache it
|
||||||
const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion();
|
const activeKeyBackupVersion = await crypto.getActiveSessionBackupVersion();
|
||||||
this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion;
|
this.cachedKeyBackupUploadActive = !!activeKeyBackupVersion;
|
||||||
logger.debug(`Key backup upload is ${this.cachedKeyBackupUploadActive ? "active" : "inactive"}`);
|
|
||||||
|
|
||||||
return this.cachedKeyBackupUploadActive;
|
return this.cachedKeyBackupUploadActive;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Import i18n.tsx instead of languageHandler to avoid circular deps
|
import { _td, type TranslationKey } from "../languageHandler";
|
||||||
import { _td, type TranslationKey } from "../shared-components/i18n";
|
|
||||||
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
|
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
|
||||||
import { type IBaseSetting } from "../settings/Settings";
|
import { type IBaseSetting } from "../settings/Settings";
|
||||||
import { type KeyCombo } from "../KeyBindingsManager";
|
import { type KeyCombo } from "../KeyBindingsManager";
|
||||||
@@ -146,7 +145,6 @@ export enum KeyBindingAction {
|
|||||||
ArrowDown = "KeyBinding.arrowDown",
|
ArrowDown = "KeyBinding.arrowDown",
|
||||||
Tab = "KeyBinding.tab",
|
Tab = "KeyBinding.tab",
|
||||||
Comma = "KeyBinding.comma",
|
Comma = "KeyBinding.comma",
|
||||||
Save = "KeyBinding.save",
|
|
||||||
|
|
||||||
/** Toggle visibility of hidden events */
|
/** Toggle visibility of hidden events */
|
||||||
ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility",
|
ToggleHiddenEventVisibility = "KeyBinding.toggleHiddenEventVisibility",
|
||||||
@@ -270,7 +268,6 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
|
|||||||
KeyBindingAction.ArrowRight,
|
KeyBindingAction.ArrowRight,
|
||||||
KeyBindingAction.ArrowDown,
|
KeyBindingAction.ArrowDown,
|
||||||
KeyBindingAction.Comma,
|
KeyBindingAction.Comma,
|
||||||
KeyBindingAction.Save,
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
[CategoryName.NAVIGATION]: {
|
[CategoryName.NAVIGATION]: {
|
||||||
@@ -623,13 +620,6 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
|
|||||||
},
|
},
|
||||||
displayName: _td("keyboard|composer_redo"),
|
displayName: _td("keyboard|composer_redo"),
|
||||||
},
|
},
|
||||||
[KeyBindingAction.Save]: {
|
|
||||||
default: {
|
|
||||||
key: Key.S,
|
|
||||||
ctrlOrCmdKey: true,
|
|
||||||
},
|
|
||||||
displayName: _td("keyboard|save"),
|
|
||||||
},
|
|
||||||
[KeyBindingAction.PreviousVisitedRoomOrSpace]: {
|
[KeyBindingAction.PreviousVisitedRoomOrSpace]: {
|
||||||
default: {
|
default: {
|
||||||
metaKey: IS_MAC,
|
metaKey: IS_MAC,
|
||||||
|
|||||||
@@ -183,30 +183,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the current selection is entirely within a single "mx_MTextBody" element.
|
|
||||||
*/
|
|
||||||
private isSelectionWithinSingleTextBody(): boolean {
|
|
||||||
const selection = window.getSelection();
|
|
||||||
if (!selection || selection.rangeCount === 0) return false;
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
|
|
||||||
function getParentByClass(node: Node | null, className: string): HTMLElement | null {
|
|
||||||
while (node) {
|
|
||||||
if (node instanceof HTMLElement && node.classList.contains(className)) {
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const startTextBody = getParentByClass(range.startContainer, "mx_MTextBody");
|
|
||||||
const endTextBody = getParentByClass(range.endContainer, "mx_MTextBody");
|
|
||||||
|
|
||||||
return !!startTextBody && startTextBody === endTextBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
private onResendReactionsClick = (): void => {
|
private onResendReactionsClick = (): void => {
|
||||||
for (const reaction of this.getUnsentReactions()) {
|
for (const reaction of this.getUnsentReactions()) {
|
||||||
Resend.resend(MatrixClientPeg.safeGet(), reaction);
|
Resend.resend(MatrixClientPeg.safeGet(), reaction);
|
||||||
@@ -303,24 +279,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onQuoteClick = (): void => {
|
|
||||||
const selectedText = getSelectedText();
|
|
||||||
if (selectedText) {
|
|
||||||
// Format as markdown quote
|
|
||||||
const quotedText = selectedText
|
|
||||||
.trim()
|
|
||||||
.split(/\r?\n/)
|
|
||||||
.map((line) => `> ${line}`)
|
|
||||||
.join("\n");
|
|
||||||
dis.dispatch({
|
|
||||||
action: Action.ComposerInsert,
|
|
||||||
text: "\n" + quotedText + "\n\n ",
|
|
||||||
timelineRenderingType: this.context.timelineRenderingType,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.closeMenu();
|
|
||||||
};
|
|
||||||
|
|
||||||
private onEditClick = (): void => {
|
private onEditClick = (): void => {
|
||||||
editEvent(
|
editEvent(
|
||||||
MatrixClientPeg.safeGet(),
|
MatrixClientPeg.safeGet(),
|
||||||
@@ -591,10 +549,8 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectedText = getSelectedText();
|
|
||||||
|
|
||||||
let copyButton: JSX.Element | undefined;
|
let copyButton: JSX.Element | undefined;
|
||||||
if (rightClick && selectedText) {
|
if (rightClick && getSelectedText()) {
|
||||||
copyButton = (
|
copyButton = (
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_MessageContextMenu_iconCopy"
|
iconClassName="mx_MessageContextMenu_iconCopy"
|
||||||
@@ -605,18 +561,6 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let quoteButton: JSX.Element | undefined;
|
|
||||||
if (rightClick && selectedText && selectedText.trim().length > 0 && this.isSelectionWithinSingleTextBody()) {
|
|
||||||
quoteButton = (
|
|
||||||
<IconizedContextMenuOption
|
|
||||||
iconClassName="mx_MessageContextMenu_iconQuote"
|
|
||||||
label={_t("action|quote")}
|
|
||||||
triggerOnMouseDown={true}
|
|
||||||
onClick={this.onQuoteClick}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let editButton: JSX.Element | undefined;
|
let editButton: JSX.Element | undefined;
|
||||||
if (rightClick && canEditContent(cli, mxEvent)) {
|
if (rightClick && canEditContent(cli, mxEvent)) {
|
||||||
editButton = (
|
editButton = (
|
||||||
@@ -686,11 +630,10 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nativeItemsList: JSX.Element | undefined;
|
let nativeItemsList: JSX.Element | undefined;
|
||||||
if (copyButton || quoteButton || copyLinkButton) {
|
if (copyButton || copyLinkButton) {
|
||||||
nativeItemsList = (
|
nativeItemsList = (
|
||||||
<IconizedContextMenuOptionList>
|
<IconizedContextMenuOptionList>
|
||||||
{copyButton}
|
{copyButton}
|
||||||
{quoteButton}
|
|
||||||
{copyLinkButton}
|
{copyLinkButton}
|
||||||
</IconizedContextMenuOptionList>
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,8 +7,16 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ChangeEvent, createRef, type KeyboardEvent, type SyntheticEvent } from "react";
|
import React, {
|
||||||
|
type JSX,
|
||||||
|
type ChangeEvent,
|
||||||
|
createRef,
|
||||||
|
type KeyboardEvent,
|
||||||
|
type SyntheticEvent,
|
||||||
|
type ChangeEventHandler,
|
||||||
|
} from "react";
|
||||||
import { type Room, RoomType, JoinRule, Preset, Visibility } from "matrix-js-sdk/src/matrix";
|
import { type Room, RoomType, JoinRule, Preset, Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
|
import withValidation, { type IFieldState, type IValidationResult } from "../elements/Validation";
|
||||||
@@ -17,7 +25,6 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
|||||||
import { checkUserIsAllowedToChangeEncryption, type IOpts } from "../../../createRoom";
|
import { checkUserIsAllowedToChangeEncryption, type IOpts } from "../../../createRoom";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import RoomAliasField from "../elements/RoomAliasField";
|
import RoomAliasField from "../elements/RoomAliasField";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import BaseDialog from "../dialogs/BaseDialog";
|
import BaseDialog from "../dialogs/BaseDialog";
|
||||||
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
import JoinRuleDropdown from "../elements/JoinRuleDropdown";
|
||||||
@@ -25,7 +32,6 @@ import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
|||||||
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
||||||
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
import { privateShouldBeEncrypted } from "../../../utils/rooms";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import LabelledCheckbox from "../elements/LabelledCheckbox";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type?: RoomType;
|
type?: RoomType;
|
||||||
@@ -219,8 +225,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
this.setState({ joinRule });
|
this.setState({ joinRule });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onEncryptedChange = (isEncrypted: boolean): void => {
|
private onEncryptedChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
this.setState({ isEncrypted });
|
this.setState({ isEncrypted: evt.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onAliasChange = (alias: string): void => {
|
private onAliasChange = (alias: string): void => {
|
||||||
@@ -231,8 +237,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
|
this.setState({ detailsOpen: (ev.target as HTMLDetailsElement).open });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNoFederateChange = (noFederate: boolean): void => {
|
private onNoFederateChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
this.setState({ noFederate });
|
this.setState({ noFederate: evt.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onNameValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
private onNameValidate = async (fieldState: IFieldState): Promise<IValidationResult> => {
|
||||||
@@ -241,8 +247,8 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
private onIsPublicKnockRoomChange = (isPublicKnockRoom: boolean): void => {
|
private onIsPublicKnockRoomChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
this.setState({ isPublicKnockRoom });
|
this.setState({ isPublicKnockRoom: evt.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
private static validateRoomName = withValidation({
|
private static validateRoomName = withValidation({
|
||||||
@@ -329,11 +335,12 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
let visibilitySection: JSX.Element | undefined;
|
let visibilitySection: JSX.Element | undefined;
|
||||||
if (this.state.joinRule === JoinRule.Knock) {
|
if (this.state.joinRule === JoinRule.Knock) {
|
||||||
visibilitySection = (
|
visibilitySection = (
|
||||||
<LabelledCheckbox
|
<SettingsToggleInput
|
||||||
|
name="publish-room"
|
||||||
className="mx_CreateRoomDialog_labelledCheckbox"
|
className="mx_CreateRoomDialog_labelledCheckbox"
|
||||||
label={_t("room_settings|security|publish_room")}
|
label={_t("room_settings|security|publish_room")}
|
||||||
onChange={this.onIsPublicKnockRoomChange}
|
onChange={this.onIsPublicKnockRoomChange}
|
||||||
value={this.state.isPublicKnockRoom}
|
checked={this.state.isPublicKnockRoom}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -354,11 +361,11 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
e2eeSection = (
|
e2eeSection = (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
|
name="encryption-toggle"
|
||||||
label={_t("create_room|encryption_label")}
|
label={_t("create_room|encryption_label")}
|
||||||
onChange={this.onEncryptedChange}
|
onChange={this.onEncryptedChange}
|
||||||
value={this.state.isEncrypted}
|
checked={this.state.isEncrypted}
|
||||||
className="mx_CreateRoomDialog_e2eSwitch" // for end-to-end tests
|
|
||||||
disabled={!this.state.canChangeEncryption}
|
disabled={!this.state.canChangeEncryption}
|
||||||
/>
|
/>
|
||||||
<p>{microcopy}</p>
|
<p>{microcopy}</p>
|
||||||
@@ -392,7 +399,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
title={title}
|
title={title}
|
||||||
screenName="CreateRoom"
|
screenName="CreateRoom"
|
||||||
>
|
>
|
||||||
<form onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
<Form.Root onSubmit={this.onOk} onKeyDown={this.onKeyDown}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<Field
|
<Field
|
||||||
ref={this.nameField}
|
ref={this.nameField}
|
||||||
@@ -431,17 +438,18 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
|||||||
<summary className="mx_CreateRoomDialog_details_summary">
|
<summary className="mx_CreateRoomDialog_details_summary">
|
||||||
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
|
{this.state.detailsOpen ? _t("action|hide_advanced") : _t("action|show_advanced")}
|
||||||
</summary>
|
</summary>
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
|
name="unfederated"
|
||||||
label={_t("create_room|unfederated", {
|
label={_t("create_room|unfederated", {
|
||||||
serverName: MatrixClientPeg.safeGet().getDomain(),
|
serverName: MatrixClientPeg.safeGet().getDomain(),
|
||||||
})}
|
})}
|
||||||
onChange={this.onNoFederateChange}
|
onChange={this.onNoFederateChange}
|
||||||
value={this.state.noFederate}
|
checked={this.state.noFederate}
|
||||||
|
helpMessage={federateLabel}
|
||||||
/>
|
/>
|
||||||
<p>{federateLabel}</p>
|
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Form.Root>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={
|
primaryButton={
|
||||||
isVideoRoom ? _t("create_room|action_create_video_room") : _t("create_room|action_create_room")
|
isVideoRoom ? _t("create_room|action_create_video_room") : _t("create_room|action_create_room")
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ChangeEventHandler, useCallback, useState } from "react";
|
import React, { type ChangeEventHandler, useCallback, useState } from "react";
|
||||||
import { Field, Label, Root } from "@vector-im/compound-web";
|
import { Field, Label, Root, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
onFinished: (shouldReject: boolean, ignoreUser: boolean, reportRoom: false | string) => void;
|
onFinished: (shouldReject: boolean, ignoreUser: boolean, reportRoom: false | string) => void;
|
||||||
@@ -22,6 +21,14 @@ export const DeclineAndBlockInviteDialog: React.FunctionComponent<IProps> = ({ o
|
|||||||
const [shouldReport, setShouldReport] = useState<boolean>(false);
|
const [shouldReport, setShouldReport] = useState<boolean>(false);
|
||||||
const [ignoreUser, setIgnoreUser] = useState<boolean>(false);
|
const [ignoreUser, setIgnoreUser] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onShouldReportChanged = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(e) => setShouldReport(e.target.checked),
|
||||||
|
[setShouldReport],
|
||||||
|
);
|
||||||
|
const onIgnoreUserChanged = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(e) => setIgnoreUser(e.target.checked),
|
||||||
|
[setIgnoreUser],
|
||||||
|
);
|
||||||
const [reportReason, setReportReason] = useState<string>("");
|
const [reportReason, setReportReason] = useState<string>("");
|
||||||
const reportReasonChanged = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
|
const reportReasonChanged = useCallback<ChangeEventHandler<HTMLTextAreaElement>>(
|
||||||
(e) => setReportReason(e.target.value),
|
(e) => setReportReason(e.target.value),
|
||||||
@@ -43,17 +50,19 @@ export const DeclineAndBlockInviteDialog: React.FunctionComponent<IProps> = ({ o
|
|||||||
>
|
>
|
||||||
<Root>
|
<Root>
|
||||||
<p>{_t("decline_invitation_dialog|confirm", { roomName })}</p>
|
<p>{_t("decline_invitation_dialog|confirm", { roomName })}</p>
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
|
name="ignore-user"
|
||||||
label={_t("report_content|ignore_user")}
|
label={_t("report_content|ignore_user")}
|
||||||
onChange={setIgnoreUser}
|
onChange={onIgnoreUserChanged}
|
||||||
caption={_t("decline_invitation_dialog|ignore_user_help")}
|
helpMessage={_t("decline_invitation_dialog|ignore_user_help")}
|
||||||
value={ignoreUser}
|
checked={ignoreUser}
|
||||||
/>
|
/>
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
|
name="report-room"
|
||||||
label={_t("action|report_room")}
|
label={_t("action|report_room")}
|
||||||
onChange={setShouldReport}
|
onChange={onShouldReportChanged}
|
||||||
caption={_t("decline_invitation_dialog|report_room_description")}
|
helpMessage={_t("decline_invitation_dialog|report_room_description")}
|
||||||
value={shouldReport}
|
checked={shouldReport}
|
||||||
/>
|
/>
|
||||||
<Field name="report-reason" aria-disabled={!shouldReport}>
|
<Field name="report-reason" aria-disabled={!shouldReport}>
|
||||||
<Label htmlFor="mx_DeclineAndBlockInviteDialog_reason">
|
<Label htmlFor="mx_DeclineAndBlockInviteDialog_reason">
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, useState } from "react";
|
import React, { type JSX, useState } from "react";
|
||||||
|
import { Form } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
@@ -97,13 +98,18 @@ const DevtoolsDialog: React.FC<IProps> = ({ roomId, threadRootId, onFinished })
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div>
|
<Form.Root
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<h3>{_t("common|options")}</h3>
|
<h3>{_t("common|options")}</h3>
|
||||||
<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} />
|
||||||
<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} />
|
<SettingsField settingKey="Developer.elementCallUrl" level={SettingLevel.DEVICE} />
|
||||||
</div>
|
</Form.Root>
|
||||||
</BaseTool>
|
</BaseTool>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,15 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ChangeEventHandler, useCallback, useState } from "react";
|
import React, { type JSX, type ChangeEventHandler, useCallback, useState } from "react";
|
||||||
import { Root, Field, Label, InlineSpinner, ErrorMessage, HelpMessage } from "@vector-im/compound-web";
|
import {
|
||||||
|
Root,
|
||||||
|
Field,
|
||||||
|
Label,
|
||||||
|
InlineSpinner,
|
||||||
|
ErrorMessage,
|
||||||
|
HelpMessage,
|
||||||
|
SettingsToggleInput,
|
||||||
|
} from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
@@ -14,7 +22,6 @@ import Markdown from "../../../Markdown";
|
|||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
@@ -33,6 +40,10 @@ export const ReportRoomDialog: React.FC<IProps> = function ({ roomId, onFinished
|
|||||||
const client = MatrixClientPeg.safeGet();
|
const client = MatrixClientPeg.safeGet();
|
||||||
|
|
||||||
const onReasonChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => setReason(e.target.value), []);
|
const onReasonChange = useCallback<ChangeEventHandler<HTMLTextAreaElement>>((e) => setReason(e.target.value), []);
|
||||||
|
const onLeaveRoomChanged = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(e) => setLeaveRoom(e.target.checked),
|
||||||
|
[setLeaveRoom],
|
||||||
|
);
|
||||||
const onCancel = useCallback(() => onFinished(false), [onFinished]);
|
const onCancel = useCallback(() => onFinished(false), [onFinished]);
|
||||||
const onSubmit = useCallback(async () => {
|
const onSubmit = useCallback(async () => {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
@@ -78,10 +89,11 @@ export const ReportRoomDialog: React.FC<IProps> = function ({ roomId, onFinished
|
|||||||
</Field>
|
</Field>
|
||||||
{adminMessage}
|
{adminMessage}
|
||||||
{busy ? <InlineSpinner /> : null}
|
{busy ? <InlineSpinner /> : null}
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
|
name="leave-room"
|
||||||
label={_t("room_list|more_options|leave_room")}
|
label={_t("room_list|more_options|leave_room")}
|
||||||
value={leaveRoom}
|
checked={leaveRoom}
|
||||||
onChange={setLeaveRoom}
|
onChange={onLeaveRoomChanged}
|
||||||
/>
|
/>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("action|send_report")}
|
primaryButton={_t("action|send_report")}
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ReactNode, type SyntheticEvent } from "react";
|
import React, { type ChangeEventHandler, type JSX, type ReactNode, type SyntheticEvent } from "react";
|
||||||
import { EventType, JoinRule } from "matrix-js-sdk/src/matrix";
|
import { EventType, JoinRule } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import BugReportDialog from "./BugReportDialog";
|
import BugReportDialog from "./BugReportDialog";
|
||||||
@@ -87,8 +87,8 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
|
|||||||
this.props.onFinished({ continue: false, invite: false });
|
this.props.onFinished({ continue: false, invite: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onInviteUsersToggle = (inviteUsersToNewRoom: boolean): void => {
|
private onInviteUsersToggle: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
this.setState({ inviteUsersToNewRoom });
|
this.setState({ inviteUsersToNewRoom: evt.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
private openBugReportDialog = (e: SyntheticEvent): void => {
|
private openBugReportDialog = (e: SyntheticEvent): void => {
|
||||||
@@ -104,11 +104,19 @@ export default class RoomUpgradeWarningDialog extends React.Component<IProps, IS
|
|||||||
let inviteToggle: JSX.Element | undefined;
|
let inviteToggle: JSX.Element | undefined;
|
||||||
if (this.isInviteOrKnockRoom) {
|
if (this.isInviteOrKnockRoom) {
|
||||||
inviteToggle = (
|
inviteToggle = (
|
||||||
<LabelledToggleSwitch
|
<Form.Root
|
||||||
value={this.state.inviteUsersToNewRoom}
|
onSubmit={(evt) => {
|
||||||
onChange={this.onInviteUsersToggle}
|
evt.preventDefault();
|
||||||
label={_t("room_settings|advanced|upgrade_warning_dialog_invite_label")}
|
evt.stopPropagation();
|
||||||
/>
|
}}
|
||||||
|
>
|
||||||
|
<SettingsToggleInput
|
||||||
|
name="room-upgrade-warning"
|
||||||
|
checked={this.state.inviteUsersToNewRoom}
|
||||||
|
onChange={this.onInviteUsersToggle}
|
||||||
|
label={_t("room_settings|advanced|upgrade_warning_dialog_invite_label")}
|
||||||
|
/>
|
||||||
|
</Form.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type ChangeEventHandler } from "react";
|
||||||
import {
|
import {
|
||||||
type Capability,
|
type Capability,
|
||||||
isTimelineCapability,
|
isTimelineCapability,
|
||||||
@@ -15,13 +15,13 @@ import {
|
|||||||
type WidgetKind,
|
type WidgetKind,
|
||||||
} from "matrix-widget-api";
|
} from "matrix-widget-api";
|
||||||
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
|
import { lexicographicCompare } from "matrix-js-sdk/src/utils";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { objectShallowClone } from "../../../utils/objects";
|
import { objectShallowClone } from "../../../utils/objects";
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import { CapabilityText } from "../../../widgets/CapabilityText";
|
import { CapabilityText } from "../../../widgets/CapabilityText";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
@@ -64,8 +64,8 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
|||||||
this.setState({ booleanStates: newStates });
|
this.setState({ booleanStates: newStates });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onRememberSelectionChange = (newVal: boolean): void => {
|
private onRememberSelectionChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
this.setState({ rememberSelection: newVal });
|
this.setState({ rememberSelection: evt.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSubmit = async (): Promise<void> => {
|
private onSubmit = async (): Promise<void> => {
|
||||||
@@ -116,7 +116,7 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
|||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={_t("widget|capabilities_dialog|title")}
|
title={_t("widget|capabilities_dialog|title")}
|
||||||
>
|
>
|
||||||
<form onSubmit={this.onSubmit}>
|
<Form.Root onSubmit={this.onSubmit}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="text-muted">{_t("widget|capabilities_dialog|content_starting_text")}</div>
|
<div className="text-muted">{_t("widget|capabilities_dialog|content_starting_text")}</div>
|
||||||
{checkboxRows}
|
{checkboxRows}
|
||||||
@@ -126,16 +126,16 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<
|
|||||||
onPrimaryButtonClick={this.onSubmit}
|
onPrimaryButtonClick={this.onSubmit}
|
||||||
onCancel={this.onReject}
|
onCancel={this.onReject}
|
||||||
additive={
|
additive={
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
value={this.state.rememberSelection}
|
name="remember-selection"
|
||||||
toggleInFront={true}
|
checked={this.state.rememberSelection}
|
||||||
onChange={this.onRememberSelectionChange}
|
onChange={this.onRememberSelectionChange}
|
||||||
label={_t("widget|capabilities_dialog|remember_Selection")}
|
label={_t("widget|capabilities_dialog|remember_Selection")}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</Form.Root>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type ChangeEventHandler } from "react";
|
||||||
import { type Widget, type WidgetKind } from "matrix-widget-api";
|
import { type Widget, type WidgetKind } from "matrix-widget-api";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import { OIDCState } from "../../../stores/widgets/WidgetPermissionStore";
|
import { OIDCState } from "../../../stores/widgets/WidgetPermissionStore";
|
||||||
import BaseDialog from "./BaseDialog";
|
import BaseDialog from "./BaseDialog";
|
||||||
import DialogButtons from "../elements/DialogButtons";
|
import DialogButtons from "../elements/DialogButtons";
|
||||||
@@ -61,8 +61,8 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
|||||||
this.props.onFinished(allowed);
|
this.props.onFinished(allowed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRememberSelectionChange = (newVal: boolean): void => {
|
private onRememberSelectionChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
this.setState({ rememberSelection: newVal });
|
this.setState({ rememberSelection: evt.target.checked });
|
||||||
};
|
};
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
@@ -85,12 +85,19 @@ export default class WidgetOpenIDPermissionsDialog extends React.PureComponent<I
|
|||||||
onPrimaryButtonClick={this.onAllow}
|
onPrimaryButtonClick={this.onAllow}
|
||||||
onCancel={this.onDeny}
|
onCancel={this.onDeny}
|
||||||
additive={
|
additive={
|
||||||
<LabelledToggleSwitch
|
<Form.Root
|
||||||
value={this.state.rememberSelection}
|
onSubmit={(evt) => {
|
||||||
toggleInFront={true}
|
evt.preventDefault();
|
||||||
onChange={this.onRememberSelectionChange}
|
evt.stopPropagation();
|
||||||
label={_t("widget|open_id_permissions_dialog|remember_selection")}
|
}}
|
||||||
/>
|
>
|
||||||
|
<SettingsToggleInput
|
||||||
|
name="remember-selection"
|
||||||
|
checked={this.state.rememberSelection}
|
||||||
|
onChange={this.onRememberSelectionChange}
|
||||||
|
label={_t("widget|open_id_permissions_dialog|remember_selection")}
|
||||||
|
/>
|
||||||
|
</Form.Root>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import FilteredList from "./FilteredList";
|
|||||||
import Spinner from "../../elements/Spinner";
|
import Spinner from "../../elements/Spinner";
|
||||||
import SyntaxHighlight from "../../elements/SyntaxHighlight";
|
import SyntaxHighlight from "../../elements/SyntaxHighlight";
|
||||||
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../../../hooks/useAsyncMemo";
|
||||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
|
||||||
|
|
||||||
export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => {
|
export const StateEventEditor: React.FC<IEditorProps> = ({ mxEvent, onBack }) => {
|
||||||
const context = useContext(DevtoolsContext);
|
const context = useContext(DevtoolsContext);
|
||||||
@@ -115,7 +114,6 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [event, setEvent] = useState<MatrixEvent | null>(null);
|
const [event, setEvent] = useState<MatrixEvent | null>(null);
|
||||||
const [history, setHistory] = useState(false);
|
const [history, setHistory] = useState(false);
|
||||||
const [hideEmptyState, setHideEmptyState] = useState(false);
|
|
||||||
|
|
||||||
const events = context.room.currentState.events.get(eventType)!;
|
const events = context.room.currentState.events.get(eventType)!;
|
||||||
|
|
||||||
@@ -151,13 +149,10 @@ const RoomStateExplorerEventType: React.FC<IEventTypeProps> = ({ eventType, onBa
|
|||||||
return (
|
return (
|
||||||
<BaseTool onBack={onBack}>
|
<BaseTool onBack={onBack}>
|
||||||
<FilteredList query={query} onChange={setQuery}>
|
<FilteredList query={query} onChange={setQuery}>
|
||||||
{Array.from(events.entries())
|
{Array.from(events.entries()).map(([stateKey, ev]) => (
|
||||||
.filter(([_, ev]) => !hideEmptyState || Object.keys(ev.getContent()).length > 0)
|
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
|
||||||
.map(([stateKey, ev]) => (
|
))}
|
||||||
<StateEventButton key={stateKey} label={stateKey} onClick={() => setEvent(ev)} />
|
|
||||||
))}
|
|
||||||
</FilteredList>
|
</FilteredList>
|
||||||
<LabelledToggleSwitch label={_t("devtools|hide_empty_content_events")} onChange={setHideEmptyState} value={hideEmptyState} />
|
|
||||||
</BaseTool>
|
</BaseTool>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, createRef, type CSSProperties, useEffect } from "react";
|
import React, { type JSX, createRef, type CSSProperties, useRef, useState, useMemo, useEffect } from "react";
|
||||||
import FocusLock from "react-focus-lock";
|
import FocusLock from "react-focus-lock";
|
||||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { type MatrixEvent, parseErrorResponse } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import MemberAvatar from "../avatars/MemberAvatar";
|
import MemberAvatar from "../avatars/MemberAvatar";
|
||||||
@@ -30,7 +31,11 @@ import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
|
|||||||
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
|
||||||
import { presentableTextForFile } from "../../../utils/FileUtils";
|
import { presentableTextForFile } from "../../../utils/FileUtils";
|
||||||
import AccessibleButton from "./AccessibleButton";
|
import AccessibleButton from "./AccessibleButton";
|
||||||
import { useDownloadMedia } from "../../../hooks/useDownloadMedia.ts";
|
import Modal from "../../../Modal";
|
||||||
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||||
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper.ts";
|
||||||
|
import ModuleApi from "../../../modules/Api";
|
||||||
|
|
||||||
// Max scale to keep gaps around the image
|
// Max scale to keep gaps around the image
|
||||||
const MAX_SCALE = 0.95;
|
const MAX_SCALE = 0.95;
|
||||||
@@ -118,8 +123,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
private imageWrapper = createRef<HTMLDivElement>();
|
private imageWrapper = createRef<HTMLDivElement>();
|
||||||
private image = createRef<HTMLImageElement>();
|
private image = createRef<HTMLImageElement>();
|
||||||
|
|
||||||
private downloadFunction?: () => Promise<void>;
|
|
||||||
|
|
||||||
private initX = 0;
|
private initX = 0;
|
||||||
private initY = 0;
|
private initY = 0;
|
||||||
private previousX = 0;
|
private previousX = 0;
|
||||||
@@ -299,13 +302,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
break;
|
break;
|
||||||
case KeyBindingAction.Save:
|
|
||||||
ev.preventDefault();
|
|
||||||
ev.stopPropagation();
|
|
||||||
if (this.downloadFunction) {
|
|
||||||
this.downloadFunction();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -331,10 +327,6 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDownloadFunctionReady = (download: () => Promise<void>): void => {
|
|
||||||
this.downloadFunction = download;
|
|
||||||
};
|
|
||||||
|
|
||||||
private onPermalinkClicked = (ev: React.MouseEvent): void => {
|
private onPermalinkClicked = (ev: React.MouseEvent): void => {
|
||||||
// This allows the permalink to be opened in a new tab/window or copied as
|
// This allows the permalink to be opened in a new tab/window or copied as
|
||||||
// matrix.to, but also for it to enable routing within Element when clicked.
|
// matrix.to, but also for it to enable routing within Element when clicked.
|
||||||
@@ -560,12 +552,7 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
title={_t("lightbox|rotate_right")}
|
title={_t("lightbox|rotate_right")}
|
||||||
onClick={this.onRotateClockwiseClick}
|
onClick={this.onRotateClockwiseClick}
|
||||||
/>
|
/>
|
||||||
<DownloadButton
|
<DownloadButton url={this.props.src} fileName={this.props.name} mxEvent={this.props.mxEvent} />
|
||||||
url={this.props.src}
|
|
||||||
fileName={this.props.name}
|
|
||||||
mxEvent={this.props.mxEvent}
|
|
||||||
onDownloadReady={this.onDownloadFunctionReady}
|
|
||||||
/>
|
|
||||||
{contextMenuButton}
|
{contextMenuButton}
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_close"
|
className="mx_ImageView_button mx_ImageView_button_close"
|
||||||
@@ -598,28 +585,97 @@ export default class ImageView extends React.Component<IProps, IState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DownloadButtonProps {
|
function DownloadButton({
|
||||||
|
url,
|
||||||
|
fileName,
|
||||||
|
mxEvent,
|
||||||
|
}: {
|
||||||
url: string;
|
url: string;
|
||||||
fileName?: string;
|
fileName?: string;
|
||||||
mxEvent?: MatrixEvent;
|
mxEvent?: MatrixEvent;
|
||||||
onDownloadReady?: (download: () => Promise<void>) => void;
|
}): JSX.Element | null {
|
||||||
}
|
const downloader = useRef(new FileDownloader()).current;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
export const DownloadButton: React.FC<DownloadButtonProps> = ({ url, fileName, mxEvent, onDownloadReady }) => {
|
const [canDownload, setCanDownload] = useState<boolean>(false);
|
||||||
const { download, loading, canDownload } = useDownloadMedia(url, fileName, mxEvent);
|
const blobRef = useRef<Blob>(undefined);
|
||||||
|
const mediaEventHelper = useMemo(() => (mxEvent ? new MediaEventHelper(mxEvent) : undefined), [mxEvent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onDownloadReady) onDownloadReady(download);
|
if (!mxEvent) {
|
||||||
}, [download, onDownloadReady]);
|
// If we have no event, we assume this is safe to download.
|
||||||
|
setCanDownload(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const hints = ModuleApi.customComponents.getHintsForMessage(mxEvent);
|
||||||
|
if (hints?.allowDownloadingMedia) {
|
||||||
|
// Disable downloading as soon as we know there is a hint.
|
||||||
|
setCanDownload(false);
|
||||||
|
hints
|
||||||
|
.allowDownloadingMedia()
|
||||||
|
.then((downloadable) => {
|
||||||
|
setCanDownload(downloadable);
|
||||||
|
})
|
||||||
|
.catch((ex) => {
|
||||||
|
logger.error(`Failed to check if media from ${mxEvent.getId()} could be downloaded`, ex);
|
||||||
|
// Err on the side of safety.
|
||||||
|
setCanDownload(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [mxEvent]);
|
||||||
|
|
||||||
if (!canDownload) return null;
|
function showError(e: unknown): void {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("timeline|download_failed"),
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<div>{_t("timeline|download_failed_description")}</div>
|
||||||
|
<div>{e instanceof Error ? e.toString() : ""}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDownloadClick = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (loading) return;
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (blobRef.current) {
|
||||||
|
// Cheat and trigger a download, again.
|
||||||
|
return downloadBlob(blobRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
throw parseErrorResponse(res, await res.text());
|
||||||
|
}
|
||||||
|
const blob = await res.blob();
|
||||||
|
blobRef.current = blob;
|
||||||
|
await downloadBlob(blob);
|
||||||
|
} catch (e) {
|
||||||
|
showError(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function downloadBlob(blob: Blob): Promise<void> {
|
||||||
|
await downloader.download({
|
||||||
|
blob,
|
||||||
|
name: mediaEventHelper?.fileName ?? fileName ?? _t("common|image"),
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canDownload) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className="mx_ImageView_button mx_ImageView_button_download"
|
className="mx_ImageView_button mx_ImageView_button_download"
|
||||||
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
||||||
onClick={download}
|
onClick={onDownloadClick}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -24,11 +24,13 @@ interface IProps {
|
|||||||
onChange(checked: boolean): void;
|
onChange(checked: boolean): void;
|
||||||
// Optional additional CSS class to apply to the label
|
// Optional additional CSS class to apply to the label
|
||||||
className?: string;
|
className?: string;
|
||||||
|
// The id for the checkbox
|
||||||
|
id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange, className }) => {
|
const LabelledCheckbox: React.FC<IProps> = ({ value, label, byline, disabled, onChange, className, id }) => {
|
||||||
return (
|
return (
|
||||||
<div className={classnames("mx_LabelledCheckbox", className)}>
|
<div id={id} className={classnames("mx_LabelledCheckbox", className)}>
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
description={byline}
|
description={byline}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2024 New Vector Ltd.
|
|
||||||
Copyright 2019-2022 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
|
||||||
Please see LICENSE files in the repository root for full details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import React, { type FC, useId } from "react";
|
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
import ToggleSwitch from "./ToggleSwitch";
|
|
||||||
import { Caption } from "../typography/Caption";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
// The value for the toggle switch
|
|
||||||
"value": boolean;
|
|
||||||
// The translated label for the switch
|
|
||||||
"label": string;
|
|
||||||
// The translated caption for the switch
|
|
||||||
"caption"?: string;
|
|
||||||
// Tooltip to display
|
|
||||||
"tooltip"?: string;
|
|
||||||
// Whether or not to disable the toggle switch
|
|
||||||
"disabled"?: boolean;
|
|
||||||
// True to put the toggle in front of the label
|
|
||||||
// Default false.
|
|
||||||
"toggleInFront"?: boolean;
|
|
||||||
// Additional class names to append to the switch. Optional.
|
|
||||||
"className"?: string;
|
|
||||||
// The function to call when the value changes
|
|
||||||
onChange(checked: boolean): void;
|
|
||||||
|
|
||||||
"data-testid"?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const LabelledToggleSwitch: FC<IProps> = ({
|
|
||||||
label,
|
|
||||||
caption,
|
|
||||||
value,
|
|
||||||
disabled,
|
|
||||||
onChange,
|
|
||||||
tooltip,
|
|
||||||
toggleInFront,
|
|
||||||
className,
|
|
||||||
"data-testid": testId,
|
|
||||||
}) => {
|
|
||||||
// This is a minimal version of a SettingsFlag
|
|
||||||
const generatedId = useId();
|
|
||||||
const id = `mx_LabelledToggleSwitch_${generatedId}`;
|
|
||||||
let firstPart = (
|
|
||||||
<span className="mx_SettingsFlag_label">
|
|
||||||
<div id={id}>{label}</div>
|
|
||||||
{caption && <Caption id={`${id}_caption`}>{caption}</Caption>}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
let secondPart = (
|
|
||||||
<ToggleSwitch
|
|
||||||
checked={value}
|
|
||||||
disabled={disabled}
|
|
||||||
onChange={onChange}
|
|
||||||
tooltip={tooltip}
|
|
||||||
aria-labelledby={id}
|
|
||||||
aria-describedby={caption ? `${id}_caption` : undefined}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (toggleInFront) {
|
|
||||||
[firstPart, secondPart] = [secondPart, firstPart];
|
|
||||||
}
|
|
||||||
|
|
||||||
const classes = classNames("mx_SettingsFlag", className, {
|
|
||||||
mx_SettingsFlag_toggleInFront: toggleInFront,
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<div data-testid={testId} className={classes}>
|
|
||||||
{firstPart}
|
|
||||||
{secondPart}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LabelledToggleSwitch;
|
|
||||||
@@ -7,13 +7,13 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type ChangeEvent } from "react";
|
||||||
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
import { secureRandomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import ToggleSwitch from "./ToggleSwitch";
|
|
||||||
import StyledCheckbox from "./StyledCheckbox";
|
|
||||||
import { type SettingLevel } from "../../../settings/SettingLevel";
|
import { type SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import { type BooleanSettingKey, defaultWatchManager } from "../../../settings/Settings";
|
import { type BooleanSettingKey, defaultWatchManager } from "../../../settings/Settings";
|
||||||
|
|
||||||
@@ -24,8 +24,6 @@ interface IProps {
|
|||||||
roomId?: string; // for per-room settings
|
roomId?: string; // for per-room settings
|
||||||
label?: string;
|
label?: string;
|
||||||
isExplicit?: boolean;
|
isExplicit?: boolean;
|
||||||
// XXX: once design replaces all toggles make this the default
|
|
||||||
useCheckbox?: boolean;
|
|
||||||
hideIfCannotSet?: boolean;
|
hideIfCannotSet?: boolean;
|
||||||
onChange?(checked: boolean): void;
|
onChange?(checked: boolean): void;
|
||||||
}
|
}
|
||||||
@@ -74,14 +72,16 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onChange = async (checked: boolean): Promise<void> => {
|
private onChange = async (evt: ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||||
await this.save(checked);
|
const newValue = evt.target.checked;
|
||||||
this.setState({ value: checked });
|
try {
|
||||||
this.props.onChange?.(checked);
|
await this.save(newValue);
|
||||||
};
|
} catch (ex) {
|
||||||
|
logger.info(`Failed to save setting ${this.props.name}`, ex);
|
||||||
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
return;
|
||||||
this.onChange(e.target.checked);
|
}
|
||||||
|
this.setState({ value: newValue });
|
||||||
|
this.props.onChange?.(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
private save = async (val?: boolean): Promise<void> => {
|
private save = async (val?: boolean): Promise<void> => {
|
||||||
@@ -101,45 +101,37 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
|||||||
const label = this.props.label ?? SettingsStore.getDisplayName(this.props.name, this.props.level);
|
const label = this.props.label ?? SettingsStore.getDisplayName(this.props.name, this.props.level);
|
||||||
const description = SettingsStore.getDescription(this.props.name);
|
const description = SettingsStore.getDescription(this.props.name);
|
||||||
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
|
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
|
||||||
|
const helpMessage = shouldWarn
|
||||||
if (this.props.useCheckbox) {
|
? _t(
|
||||||
return (
|
"settings|warning",
|
||||||
<StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={disabled}>
|
{},
|
||||||
{label}
|
{
|
||||||
</StyledCheckbox>
|
w: (sub) => <span className="mx_SettingsTab_microcopy_warning">{sub}</span>,
|
||||||
);
|
description,
|
||||||
} else {
|
},
|
||||||
return (
|
)
|
||||||
<div className="mx_SettingsFlag">
|
: description;
|
||||||
<label className="mx_SettingsFlag_label" htmlFor={this.id}>
|
if (this.props.name === "sendReadReceipts") {
|
||||||
<span className="mx_SettingsFlag_labelText">{label}</span>
|
console.log(
|
||||||
{description && (
|
"Fully disabled",
|
||||||
<div className="mx_SettingsFlag_microcopy">
|
this.props.name,
|
||||||
{shouldWarn
|
SettingsStore.disabledMessage(this.props.name),
|
||||||
? _t(
|
description,
|
||||||
"settings|warning",
|
helpMessage,
|
||||||
{},
|
|
||||||
{
|
|
||||||
w: (sub) => (
|
|
||||||
<span className="mx_SettingsTab_microcopy_warning">{sub}</span>
|
|
||||||
),
|
|
||||||
description,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<ToggleSwitch
|
|
||||||
id={this.id}
|
|
||||||
checked={this.state.value}
|
|
||||||
onChange={this.onChange}
|
|
||||||
disabled={disabled}
|
|
||||||
tooltip={disabled ? SettingsStore.disabledMessage(this.props.name) : undefined}
|
|
||||||
title={label ?? undefined}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
const disabledMessage = SettingsStore.disabledMessage(this.props.name);
|
||||||
|
return (
|
||||||
|
<SettingsToggleInput
|
||||||
|
id={this.id}
|
||||||
|
checked={this.state.value}
|
||||||
|
onChange={disabledMessage ? undefined : this.onChange}
|
||||||
|
name={this.props.name}
|
||||||
|
disabled={disabled}
|
||||||
|
label={label ?? this.props.name}
|
||||||
|
helpMessage={helpMessage as string}
|
||||||
|
disabledMessage={disabledMessage}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { type ChangeEventHandler, type FormEventHandler, useCallback, useState } from "react";
|
||||||
|
import { SettingsToggleInput, Form, Button } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import StyledLiveBeaconIcon from "../beacon/StyledLiveBeaconIcon";
|
import StyledLiveBeaconIcon from "../beacon/StyledLiveBeaconIcon";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import Heading from "../typography/Heading";
|
import Heading from "../typography/Heading";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,6 +19,25 @@ interface Props {
|
|||||||
|
|
||||||
export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
|
export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
|
||||||
const [isEnabled, setEnabled] = useState(false);
|
const [isEnabled, setEnabled] = useState(false);
|
||||||
|
|
||||||
|
const onEnabledChanged = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
|
(e) => setEnabled(e.target.checked),
|
||||||
|
[setEnabled],
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSubmitForm = useCallback<FormEventHandler>(
|
||||||
|
(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
if (isEnabled) {
|
||||||
|
onSubmit();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[isEnabled, onSubmit],
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("EnableLiveShare", isEnabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid="location-picker-enable-live-share" className="mx_EnableLiveShare">
|
<div data-testid="location-picker-enable-live-share" className="mx_EnableLiveShare">
|
||||||
<StyledLiveBeaconIcon className="mx_EnableLiveShare_icon" />
|
<StyledLiveBeaconIcon className="mx_EnableLiveShare_icon" />
|
||||||
@@ -27,22 +45,17 @@ export const EnableLiveShare: React.FC<Props> = ({ onSubmit }) => {
|
|||||||
{_t("location_sharing|live_enable_heading")}
|
{_t("location_sharing|live_enable_heading")}
|
||||||
</Heading>
|
</Heading>
|
||||||
<p className="mx_EnableLiveShare_description">{_t("location_sharing|live_enable_description")}</p>
|
<p className="mx_EnableLiveShare_description">{_t("location_sharing|live_enable_description")}</p>
|
||||||
<LabelledToggleSwitch
|
<Form.Root onSubmit={onSubmitForm}>
|
||||||
data-testid="enable-live-share-toggle"
|
<SettingsToggleInput
|
||||||
value={isEnabled}
|
name="enable-live-share-toggle"
|
||||||
onChange={setEnabled}
|
checked={isEnabled}
|
||||||
label={_t("location_sharing|live_toggle_label")}
|
onChange={onEnabledChanged}
|
||||||
/>
|
label={_t("location_sharing|live_toggle_label")}
|
||||||
<AccessibleButton
|
/>
|
||||||
data-testid="enable-live-share-submit"
|
<Button className="mx_EnableLiveShare_button" kind="primary" disabled={!isEnabled}>
|
||||||
className="mx_EnableLiveShare_button"
|
{_t("action|ok")}
|
||||||
element="button"
|
</Button>
|
||||||
kind="primary"
|
</Form.Root>
|
||||||
onClick={onSubmit}
|
|
||||||
disabled={!isEnabled}
|
|
||||||
>
|
|
||||||
{_t("action|ok")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import React, { type ReactElement, useMemo } from "react";
|
import React, { type JSX } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
import { DownloadIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { type MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { type MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t, _td, type TranslationKey } from "../../../languageHandler";
|
||||||
import { useDownloadMedia } from "../../../hooks/useDownloadMedia";
|
import { FileDownloader } from "../../../utils/FileDownloader";
|
||||||
|
import Modal from "../../../Modal";
|
||||||
|
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||||
|
import ModuleApi from "../../../modules/Api";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
@@ -26,32 +30,121 @@ interface IProps {
|
|||||||
mediaEventHelperGet: () => MediaEventHelper | undefined;
|
mediaEventHelperGet: () => MediaEventHelper | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DownloadActionButton({ mxEvent, mediaEventHelperGet }: IProps): ReactElement | null {
|
interface IState {
|
||||||
const mediaEventHelper = useMemo(() => mediaEventHelperGet(), [mediaEventHelperGet]);
|
canDownload: null | boolean;
|
||||||
const downloadUrl = mediaEventHelper?.media.srcHttp ?? "";
|
loading: boolean;
|
||||||
const fileName = mediaEventHelper?.fileName;
|
blob?: Blob;
|
||||||
|
tooltip: TranslationKey;
|
||||||
const { download, loading, canDownload } = useDownloadMedia(downloadUrl, fileName, mxEvent);
|
}
|
||||||
|
|
||||||
if (!canDownload) return null;
|
export default class DownloadActionButton extends React.PureComponent<IProps, IState> {
|
||||||
|
private downloader = new FileDownloader();
|
||||||
const spinner = loading ? <Spinner w={18} h={18} /> : undefined;
|
|
||||||
const classes = classNames({
|
public constructor(props: IProps) {
|
||||||
mx_MessageActionBar_iconButton: true,
|
super(props);
|
||||||
mx_MessageActionBar_downloadButton: true,
|
|
||||||
mx_MessageActionBar_downloadSpinnerButton: !!spinner,
|
const moduleHints = ModuleApi.customComponents.getHintsForMessage(props.mxEvent);
|
||||||
});
|
const downloadState: Pick<IState, "canDownload"> = { canDownload: true };
|
||||||
|
if (moduleHints?.allowDownloadingMedia) {
|
||||||
return (
|
downloadState.canDownload = null;
|
||||||
<RovingAccessibleButton
|
moduleHints
|
||||||
className={classes}
|
.allowDownloadingMedia()
|
||||||
title={loading ? _t("timeline|download_action_downloading") : _t("action|download")}
|
.then((canDownload) => {
|
||||||
onClick={download}
|
this.setState({
|
||||||
disabled={loading}
|
canDownload: canDownload,
|
||||||
placement="left"
|
});
|
||||||
>
|
})
|
||||||
<DownloadIcon />
|
.catch((ex) => {
|
||||||
{spinner}
|
logger.error(`Failed to check if media from ${props.mxEvent.getId()} could be downloaded`, ex);
|
||||||
</RovingAccessibleButton>
|
this.setState({
|
||||||
);
|
canDownload: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
loading: false,
|
||||||
|
tooltip: _td("timeline|download_action_downloading"),
|
||||||
|
...downloadState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private onDownloadClick = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await this.doDownload();
|
||||||
|
} catch (e) {
|
||||||
|
Modal.createDialog(ErrorDialog, {
|
||||||
|
title: _t("timeline|download_failed"),
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<div>{_t("timeline|download_failed_description")}</div>
|
||||||
|
<div>{e instanceof Error ? e.toString() : ""}</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private async doDownload(): Promise<void> {
|
||||||
|
const mediaEventHelper = this.props.mediaEventHelperGet();
|
||||||
|
if (this.state.loading || !mediaEventHelper) return;
|
||||||
|
|
||||||
|
if (mediaEventHelper.media.isEncrypted) {
|
||||||
|
this.setState({ tooltip: _td("timeline|download_action_decrypting") });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
if (this.state.blob) {
|
||||||
|
// Cheat and trigger a download, again.
|
||||||
|
return this.downloadBlob(this.state.blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = await mediaEventHelper.sourceBlob.value;
|
||||||
|
this.setState({ blob });
|
||||||
|
await this.downloadBlob(blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async downloadBlob(blob: Blob): Promise<void> {
|
||||||
|
await this.downloader.download({
|
||||||
|
blob,
|
||||||
|
name: this.props.mediaEventHelperGet()!.fileName,
|
||||||
|
});
|
||||||
|
this.setState({ loading: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
|
let spinner: JSX.Element | undefined;
|
||||||
|
if (this.state.loading) {
|
||||||
|
spinner = <Spinner w={18} h={18} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.canDownload === null) {
|
||||||
|
spinner = <Spinner w={18} h={18} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.canDownload === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = classNames({
|
||||||
|
mx_MessageActionBar_iconButton: true,
|
||||||
|
mx_MessageActionBar_downloadButton: true,
|
||||||
|
mx_MessageActionBar_downloadSpinnerButton: !!spinner,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RovingAccessibleButton
|
||||||
|
className={classes}
|
||||||
|
title={spinner ? _t(this.state.tooltip) : _t("action|download")}
|
||||||
|
onClick={this.onDownloadClick}
|
||||||
|
disabled={!!spinner}
|
||||||
|
placement="left"
|
||||||
|
>
|
||||||
|
<DownloadIcon />
|
||||||
|
{spinner}
|
||||||
|
</RovingAccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
47
src/components/views/messages/TextualEvent.tsx
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 New Vector Ltd.
|
||||||
|
Copyright 2015-2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { type MatrixEvent, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
|
import * as TextForEvent from "../../../TextForEvent";
|
||||||
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TextualEvent extends React.Component<IProps> {
|
||||||
|
public static contextType = RoomContext;
|
||||||
|
declare public context: React.ContextType<typeof RoomContext>;
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
|
this.props.mxEvent.on(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
|
||||||
|
}
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
this.props.mxEvent.off(MatrixEventEvent.SentinelUpdated, this.onEventSentinelUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onEventSentinelUpdated = (): void => {
|
||||||
|
// XXX: this is crap, but we don't have a better way to force a re-render
|
||||||
|
// Many TextForEvent handlers render parts of `event.sender` and `event.target` so ensure they are updated
|
||||||
|
this.forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): React.ReactNode {
|
||||||
|
const text = TextForEvent.textForEvent(
|
||||||
|
this.props.mxEvent,
|
||||||
|
MatrixClientPeg.safeGet(),
|
||||||
|
true,
|
||||||
|
this.context?.showHiddenEvents,
|
||||||
|
);
|
||||||
|
if (!text) return null;
|
||||||
|
return <div className="mx_TextualEvent">{text}</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,10 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { type ChangeEventHandler } from "react";
|
||||||
import { JoinRule, Visibility } from "matrix-js-sdk/src/matrix";
|
import { JoinRule, Visibility } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import DirectoryCustomisations from "../../../customisations/Directory";
|
import DirectoryCustomisations from "../../../customisations/Directory";
|
||||||
@@ -24,6 +25,7 @@ interface IProps {
|
|||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
isRoomPublished: boolean;
|
isRoomPublished: boolean;
|
||||||
|
busy: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomPublishSetting extends React.PureComponent<IProps, IState> {
|
export default class RoomPublishSetting extends React.PureComponent<IProps, IState> {
|
||||||
@@ -32,6 +34,7 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
|||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
isRoomPublished: false,
|
isRoomPublished: false,
|
||||||
|
busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,19 +45,23 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomPublishChange = (): void => {
|
private onRoomPublishChange: ChangeEventHandler<HTMLInputElement> = async (evt): Promise<void> => {
|
||||||
const valueBefore = this.state.isRoomPublished;
|
const newValue = evt.target.checked;
|
||||||
const newValue = !valueBefore;
|
this.setState({ busy: true });
|
||||||
this.setState({ isRoomPublished: newValue });
|
|
||||||
const client = MatrixClientPeg.safeGet();
|
const client = MatrixClientPeg.safeGet();
|
||||||
|
|
||||||
client
|
try {
|
||||||
.setRoomDirectoryVisibility(this.props.roomId, newValue ? Visibility.Public : Visibility.Private)
|
await client.setRoomDirectoryVisibility(
|
||||||
.catch(() => {
|
this.props.roomId,
|
||||||
this.showError();
|
newValue ? Visibility.Public : Visibility.Private,
|
||||||
// Roll back the local echo on the change
|
);
|
||||||
this.setState({ isRoomPublished: valueBefore });
|
this.setState({ isRoomPublished: newValue });
|
||||||
});
|
} catch (ex) {
|
||||||
|
logger.error("Error while setting room directory visibility", ex);
|
||||||
|
this.showError();
|
||||||
|
} finally {
|
||||||
|
this.setState({ busy: false });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
@@ -69,17 +76,26 @@ export default class RoomPublishSetting extends React.PureComponent<IProps, ISta
|
|||||||
|
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const isRoomPublishable = room && room.getJoinRule() !== JoinRule.Invite;
|
const isRoomPublishable = room && room.getJoinRule() !== JoinRule.Invite;
|
||||||
|
const canSetCanonicalAlias =
|
||||||
|
DirectoryCustomisations.requireCanonicalAliasAccessToPublish?.() === false ||
|
||||||
|
this.props.canSetCanonicalAlias;
|
||||||
|
|
||||||
const enabled =
|
let disabledMessage;
|
||||||
(DirectoryCustomisations.requireCanonicalAliasAccessToPublish?.() === false ||
|
if (!isRoomPublishable) {
|
||||||
this.props.canSetCanonicalAlias) &&
|
disabledMessage = _t("room_settings|general|publish_warn_invite_only");
|
||||||
(isRoomPublishable || this.state.isRoomPublished);
|
} else if (!canSetCanonicalAlias) {
|
||||||
|
disabledMessage = _t("room_settings|general|publish_warn_no_canonical_permission");
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = canSetCanonicalAlias && (isRoomPublishable || this.state.isRoomPublished);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
value={this.state.isRoomPublished}
|
name="room-publish"
|
||||||
|
checked={this.state.isRoomPublished}
|
||||||
onChange={this.onRoomPublishChange}
|
onChange={this.onRoomPublishChange}
|
||||||
disabled={!enabled}
|
disabled={!enabled || this.state.busy}
|
||||||
|
disabledMessage={disabledMessage}
|
||||||
label={_t("room_settings|general|publish_toggle", {
|
label={_t("room_settings|general|publish_toggle", {
|
||||||
domain: client.getDomain(),
|
domain: client.getDomain(),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ import { getEventDisplayInfo } from "../../../utils/EventRenderingUtils";
|
|||||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||||
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
import { MediaEventHelper } from "../../../utils/MediaEventHelper";
|
||||||
import { type ButtonEvent } from "../elements/AccessibleButton";
|
import { type ButtonEvent } from "../elements/AccessibleButton";
|
||||||
import { copyPlaintext } from "../../../utils/strings";
|
import { copyPlaintext, getSelectedText } from "../../../utils/strings";
|
||||||
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
import { DecryptionFailureTracker } from "../../../DecryptionFailureTracker";
|
||||||
import RedactedBody from "../messages/RedactedBody";
|
import RedactedBody from "../messages/RedactedBody";
|
||||||
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
|
||||||
@@ -840,8 +840,10 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
// Electron layer (webcontents-handler.ts)
|
// Electron layer (webcontents-handler.ts)
|
||||||
if (clickTarget instanceof HTMLImageElement) return;
|
if (clickTarget instanceof HTMLImageElement) return;
|
||||||
|
|
||||||
// Return if we're in a browser and click either an a tag, as in those cases we want to use the native browser menu
|
// Return if we're in a browser and click either an a tag or we have
|
||||||
if (!PlatformPeg.get()?.allowOverridingNativeContextMenus() && anchorElement) return;
|
// selected text, as in those cases we want to use the native browser
|
||||||
|
// menu
|
||||||
|
if (!PlatformPeg.get()?.allowOverridingNativeContextMenus() && (getSelectedText() || anchorElement)) return;
|
||||||
|
|
||||||
// We don't want to show the menu when editing a message
|
// We don't want to show the menu when editing a message
|
||||||
if (this.props.editState) return;
|
if (this.props.editState) return;
|
||||||
@@ -1235,19 +1237,22 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
{replyChain}
|
{replyChain}
|
||||||
{renderTile(TimelineRenderingType.Thread, {
|
{renderTile(
|
||||||
...this.props,
|
TimelineRenderingType.Thread,
|
||||||
|
{
|
||||||
|
...this.props,
|
||||||
|
|
||||||
// overrides
|
// overrides
|
||||||
ref: this.tile,
|
ref: this.tile,
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
|
||||||
// appease TS
|
// appease TS
|
||||||
highlights: this.props.highlights,
|
highlights: this.props.highlights,
|
||||||
highlightLink: this.props.highlightLink,
|
highlightLink: this.props.highlightLink,
|
||||||
permalinkCreator: this.props.permalinkCreator!,
|
permalinkCreator: this.props.permalinkCreator!,
|
||||||
showHiddenEvents: this.context.showHiddenEvents,
|
},
|
||||||
})}
|
this.context.showHiddenEvents,
|
||||||
|
)}
|
||||||
{actionBar}
|
{actionBar}
|
||||||
<a href={permalink} onClick={this.onPermalinkClicked}>
|
<a href={permalink} onClick={this.onPermalinkClicked}>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
@@ -1378,19 +1383,22 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
</a>,
|
</a>,
|
||||||
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
<div className={lineClasses} key="mx_EventTile_line" onContextMenu={this.onContextMenu}>
|
||||||
{this.renderContextMenu()}
|
{this.renderContextMenu()}
|
||||||
{renderTile(TimelineRenderingType.File, {
|
{renderTile(
|
||||||
...this.props,
|
TimelineRenderingType.File,
|
||||||
|
{
|
||||||
|
...this.props,
|
||||||
|
|
||||||
// overrides
|
// overrides
|
||||||
ref: this.tile,
|
ref: this.tile,
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
|
|
||||||
// appease TS
|
// appease TS
|
||||||
highlights: this.props.highlights,
|
highlights: this.props.highlights,
|
||||||
highlightLink: this.props.highlightLink,
|
highlightLink: this.props.highlightLink,
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
showHiddenEvents: this.context.showHiddenEvents,
|
},
|
||||||
})}
|
this.context.showHiddenEvents,
|
||||||
|
)}
|
||||||
</div>,
|
</div>,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1425,20 +1433,23 @@ export class UnwrappedEventTile extends React.Component<EventTileProps, IState>
|
|||||||
{groupTimestamp}
|
{groupTimestamp}
|
||||||
{groupPadlock}
|
{groupPadlock}
|
||||||
{replyChain}
|
{replyChain}
|
||||||
{renderTile(this.context.timelineRenderingType, {
|
{renderTile(
|
||||||
...this.props,
|
this.context.timelineRenderingType,
|
||||||
|
{
|
||||||
|
...this.props,
|
||||||
|
|
||||||
// overrides
|
// overrides
|
||||||
ref: this.tile,
|
ref: this.tile,
|
||||||
isSeeingThroughMessageHiddenForModeration,
|
isSeeingThroughMessageHiddenForModeration,
|
||||||
timestamp: bubbleTimestamp,
|
timestamp: bubbleTimestamp,
|
||||||
|
|
||||||
// appease TS
|
// appease TS
|
||||||
highlights: this.props.highlights,
|
highlights: this.props.highlights,
|
||||||
highlightLink: this.props.highlightLink,
|
highlightLink: this.props.highlightLink,
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
showHiddenEvents: this.context.showHiddenEvents,
|
},
|
||||||
})}
|
this.context.showHiddenEvents,
|
||||||
|
)}
|
||||||
{actionBar}
|
{actionBar}
|
||||||
{this.props.layout === Layout.IRC && (
|
{this.props.layout === Layout.IRC && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ const MemberListView: React.FC<IProps> = (props: IProps) => {
|
|||||||
className="mx_MemberListView_container"
|
className="mx_MemberListView_container"
|
||||||
onKeyDown={onKeyDownHandler}
|
onKeyDown={onKeyDownHandler}
|
||||||
>
|
>
|
||||||
<Form.Root onSubmit={(e) => e.preventDefault()}>
|
<Form.Root>
|
||||||
<MemberListHeaderView vm={vm} />
|
<MemberListHeaderView vm={vm} />
|
||||||
</Form.Root>
|
</Form.Root>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ export default class ReplyTile extends React.PureComponent<IProps> {
|
|||||||
highlights: this.props.highlights,
|
highlights: this.props.highlights,
|
||||||
highlightLink: this.props.highlightLink,
|
highlightLink: this.props.highlightLink,
|
||||||
permalinkCreator: this.props.permalinkCreator,
|
permalinkCreator: this.props.permalinkCreator,
|
||||||
showHiddenEvents: false,
|
|
||||||
},
|
},
|
||||||
false /* showHiddenEvents shouldn't be relevant */,
|
false /* showHiddenEvents shouldn't be relevant */,
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ReactNode } from "react";
|
import React, { type ChangeEvent, type ChangeEventHandler, type JSX, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
type IAnnotatedPushRule,
|
type IAnnotatedPushRule,
|
||||||
type IPusher,
|
type IPusher,
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
type EmptyObject,
|
type EmptyObject,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
@@ -31,7 +32,6 @@ import {
|
|||||||
type VectorPushRuleDefinition,
|
type VectorPushRuleDefinition,
|
||||||
} from "../../../notifications";
|
} from "../../../notifications";
|
||||||
import { _t, type TranslatedString } from "../../../languageHandler";
|
import { _t, type TranslatedString } from "../../../languageHandler";
|
||||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import StyledRadioButton from "../elements/StyledRadioButton";
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
@@ -122,9 +122,6 @@ interface IState {
|
|||||||
threepids?: IThreepid[];
|
threepids?: IThreepid[];
|
||||||
|
|
||||||
deviceNotificationsEnabled: boolean;
|
deviceNotificationsEnabled: boolean;
|
||||||
desktopNotifications: boolean;
|
|
||||||
desktopShowBody: boolean;
|
|
||||||
audioNotifications: boolean;
|
|
||||||
|
|
||||||
clearingNotifications: boolean;
|
clearingNotifications: boolean;
|
||||||
|
|
||||||
@@ -194,10 +191,15 @@ const maximumVectorState = (
|
|||||||
|
|
||||||
const NotificationActivitySettings = (): JSX.Element => {
|
const NotificationActivitySettings = (): JSX.Element => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<Form.Root
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
<SettingsFlag name="Notifications.showbold" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="Notifications.showbold" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="Notifications.tac_only_notifications" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="Notifications.tac_only_notifications" level={SettingLevel.DEVICE} />
|
||||||
</div>
|
</Form.Root>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -213,9 +215,6 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
this.state = {
|
this.state = {
|
||||||
phase: Phase.Loading,
|
phase: Phase.Loading,
|
||||||
deviceNotificationsEnabled: SettingsStore.getValue("deviceNotificationsEnabled") ?? true,
|
deviceNotificationsEnabled: SettingsStore.getValue("deviceNotificationsEnabled") ?? true,
|
||||||
desktopNotifications: SettingsStore.getValue("notificationsEnabled"),
|
|
||||||
desktopShowBody: SettingsStore.getValue("notificationBodyEnabled"),
|
|
||||||
audioNotifications: SettingsStore.getValue("audioNotificationsEnabled"),
|
|
||||||
clearingNotifications: false,
|
clearingNotifications: false,
|
||||||
ruleIdsWithError: {},
|
ruleIdsWithError: {},
|
||||||
};
|
};
|
||||||
@@ -231,18 +230,9 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
|
|
||||||
public componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this.settingWatchers = [
|
this.settingWatchers = [
|
||||||
SettingsStore.watchSetting("notificationsEnabled", null, (...[, , , , value]) =>
|
|
||||||
this.setState({ desktopNotifications: value as boolean }),
|
|
||||||
),
|
|
||||||
SettingsStore.watchSetting("deviceNotificationsEnabled", null, (...[, , , , value]) => {
|
SettingsStore.watchSetting("deviceNotificationsEnabled", null, (...[, , , , value]) => {
|
||||||
this.setState({ deviceNotificationsEnabled: value as boolean });
|
this.setState({ deviceNotificationsEnabled: value as boolean });
|
||||||
}),
|
}),
|
||||||
SettingsStore.watchSetting("notificationBodyEnabled", null, (...[, , , , value]) =>
|
|
||||||
this.setState({ desktopShowBody: value as boolean }),
|
|
||||||
),
|
|
||||||
SettingsStore.watchSetting("audioNotificationsEnabled", null, (...[, , , , value]) =>
|
|
||||||
this.setState({ audioNotifications: value as boolean }),
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// noinspection JSIgnoredPromiseFromCall
|
// noinspection JSIgnoredPromiseFromCall
|
||||||
@@ -286,7 +276,7 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
const settingsEvent = cli.getAccountData(getLocalNotificationAccountDataEventType(cli.deviceId));
|
const settingsEvent = cli.getAccountData(getLocalNotificationAccountDataEventType(cli.deviceId));
|
||||||
if (settingsEvent) {
|
if (settingsEvent) {
|
||||||
const notificationsEnabled = !(settingsEvent.getContent() as LocalNotificationSettings).is_silenced;
|
const notificationsEnabled = !(settingsEvent.getContent() as LocalNotificationSettings).is_silenced;
|
||||||
await this.updateDeviceNotifications(notificationsEnabled);
|
await SettingsStore.setValue("deviceNotificationsEnabled", null, SettingLevel.DEVICE, notificationsEnabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -410,7 +400,8 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMasterRuleChanged = async (checked: boolean): Promise<void> => {
|
private onMasterRuleChanged: ChangeEventHandler<HTMLInputElement> = async (evt): Promise<void> => {
|
||||||
|
const { checked } = evt.target;
|
||||||
this.setState({ phase: Phase.Persisting });
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
const masterRule = this.state.masterPushRule!;
|
const masterRule = this.state.masterPushRule!;
|
||||||
@@ -431,11 +422,8 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateDeviceNotifications = async (checked: boolean): Promise<void> => {
|
private onEmailNotificationsChanged = async (email: string, evt: ChangeEvent<HTMLInputElement>): Promise<void> => {
|
||||||
await SettingsStore.setValue("deviceNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
const { checked } = evt.target;
|
||||||
};
|
|
||||||
|
|
||||||
private onEmailNotificationsChanged = async (email: string, checked: boolean): Promise<void> => {
|
|
||||||
this.setState({ phase: Phase.Persisting });
|
this.setState({ phase: Phase.Persisting });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -470,18 +458,6 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDesktopNotificationsChanged = async (checked: boolean): Promise<void> => {
|
|
||||||
await SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onDesktopShowBodyChanged = async (checked: boolean): Promise<void> => {
|
|
||||||
await SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onAudioNotificationsChanged = async (checked: boolean): Promise<void> => {
|
|
||||||
await SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
|
private onRadioChecked = async (rule: IVectorPushRule, checkedState: VectorState): Promise<void> => {
|
||||||
this.setState(({ ruleIdsWithError }) => ({
|
this.setState(({ ruleIdsWithError }) => ({
|
||||||
phase: Phase.Persisting,
|
phase: Phase.Persisting,
|
||||||
@@ -663,11 +639,11 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
|
|
||||||
private renderTopSection(): JSX.Element {
|
private renderTopSection(): JSX.Element {
|
||||||
const masterSwitch = (
|
const masterSwitch = (
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
data-testid="notif-master-switch"
|
checked={!this.isInhibited}
|
||||||
value={!this.isInhibited}
|
name="notif-master-switch"
|
||||||
label={_t("settings|notifications|enable_notifications_account")}
|
label={_t("settings|notifications|enable_notifications_account")}
|
||||||
caption={_t("settings|notifications|enable_notifications_account_detail")}
|
helpMessage={_t("settings|notifications|enable_notifications_account_detail")}
|
||||||
onChange={this.onMasterRuleChanged}
|
onChange={this.onMasterRuleChanged}
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
/>
|
/>
|
||||||
@@ -681,10 +657,10 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
const emailSwitches = (this.state.threepids || [])
|
const emailSwitches = (this.state.threepids || [])
|
||||||
.filter((t) => t.medium === ThreepidMedium.Email)
|
.filter((t) => t.medium === ThreepidMedium.Email)
|
||||||
.map((e) => (
|
.map((e) => (
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
data-testid="notif-email-switch"
|
name="notif-email-switch"
|
||||||
key={e.address}
|
key={e.address}
|
||||||
value={!!this.state.pushers?.some((p) => p.kind === "email" && p.pushkey === e.address)}
|
checked={!!this.state.pushers?.some((p) => p.kind === "email" && p.pushkey === e.address)}
|
||||||
label={_t("settings|notifications|enable_email_notifications", { email: e.address })}
|
label={_t("settings|notifications|enable_email_notifications", { email: e.address })}
|
||||||
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
onChange={this.onEmailNotificationsChanged.bind(this, e.address)}
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
disabled={this.state.phase === Phase.Persisting}
|
||||||
@@ -695,37 +671,13 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
<SettingsSubsection>
|
<SettingsSubsection>
|
||||||
{masterSwitch}
|
{masterSwitch}
|
||||||
|
|
||||||
<LabelledToggleSwitch
|
<SettingsFlag name="deviceNotificationsEnabled" level={SettingLevel.DEVICE} />
|
||||||
data-testid="notif-device-switch"
|
|
||||||
value={this.state.deviceNotificationsEnabled}
|
|
||||||
label={_t("settings|notifications|enable_notifications_device")}
|
|
||||||
onChange={(checked) => this.updateDeviceNotifications(checked)}
|
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{this.state.deviceNotificationsEnabled && (
|
{this.state.deviceNotificationsEnabled && (
|
||||||
<>
|
<>
|
||||||
<LabelledToggleSwitch
|
<SettingsFlag name="notificationsEnabled" level={SettingLevel.DEVICE} />
|
||||||
data-testid="notif-setting-notificationsEnabled"
|
<SettingsFlag name="notificationBodyEnabled" level={SettingLevel.DEVICE} />
|
||||||
value={this.state.desktopNotifications}
|
<SettingsFlag name="audioNotificationsEnabled" level={SettingLevel.DEVICE} />
|
||||||
onChange={this.onDesktopNotificationsChanged}
|
|
||||||
label={_t("settings|notifications|enable_desktop_notifications_session")}
|
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
|
||||||
/>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
data-testid="notif-setting-notificationBodyEnabled"
|
|
||||||
value={this.state.desktopShowBody}
|
|
||||||
onChange={this.onDesktopShowBodyChanged}
|
|
||||||
label={_t("settings|notifications|show_message_desktop_notification")}
|
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
|
||||||
/>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
data-testid="notif-setting-audioNotificationsEnabled"
|
|
||||||
value={this.state.audioNotifications}
|
|
||||||
onChange={this.onAudioNotificationsChanged}
|
|
||||||
label={_t("settings|notifications|enable_audible_notifications_session")}
|
|
||||||
disabled={this.state.phase === Phase.Persisting}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -868,7 +820,12 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Form.Root
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
{this.renderTopSection()}
|
{this.renderTopSection()}
|
||||||
{this.renderCategory(RuleClass.VectorGlobal)}
|
{this.renderCategory(RuleClass.VectorGlobal)}
|
||||||
{this.renderCategory(RuleClass.VectorMentions)}
|
{this.renderCategory(RuleClass.VectorMentions)}
|
||||||
@@ -876,7 +833,7 @@ export default class Notifications extends React.PureComponent<EmptyObject, ISta
|
|||||||
{this.renderTargets()}
|
{this.renderTargets()}
|
||||||
<NotificationActivitySettings />
|
<NotificationActivitySettings />
|
||||||
{clearNotifsButton}
|
{clearNotifsButton}
|
||||||
</>
|
</Form.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||||
import { Root, InlineField, Label, ToggleInput } from "@vector-im/compound-web";
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||||
@@ -66,33 +66,31 @@ export default class SetIntegrationManager extends React.Component<EmptyObject,
|
|||||||
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
if (!SettingsStore.getValue(UIFeature.Widgets)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SetIntegrationManager" data-testid="mx_SetIntegrationManager">
|
<Form.Root
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="mx_SetIntegrationManager"
|
||||||
|
data-testid="mx_SetIntegrationManager"
|
||||||
|
>
|
||||||
<div className="mx_SettingsFlag">
|
<div className="mx_SettingsFlag">
|
||||||
<div className="mx_SetIntegrationManager_heading_manager">
|
<div className="mx_SetIntegrationManager_heading_manager">
|
||||||
<Heading size="3">{_t("integration_manager|manage_title")}</Heading>
|
<Heading size="3">{_t("integration_manager|manage_title")}</Heading>
|
||||||
<Heading size="4">{managerName}</Heading>
|
<Heading id="mx_SetIntegrationManager_ManagerName" size="4">
|
||||||
|
{managerName}
|
||||||
|
</Heading>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SettingsSubsectionText>{bodyText}</SettingsSubsectionText>
|
<SettingsSubsectionText id="mx_SetIntegrationManager_BodyText">{bodyText}</SettingsSubsectionText>
|
||||||
<SettingsSubsectionText>{_t("integration_manager|explainer")}</SettingsSubsectionText>
|
<SettingsSubsectionText>{_t("integration_manager|explainer")}</SettingsSubsectionText>
|
||||||
<Root>
|
<SettingsToggleInput
|
||||||
<InlineField
|
name="enable_im"
|
||||||
name="enable_im"
|
label={_t("integration_manager|toggle_label")}
|
||||||
control={
|
checked={this.state.provisioningEnabled}
|
||||||
<ToggleInput
|
onChange={this.onProvisioningToggled}
|
||||||
role="switch"
|
/>
|
||||||
id="mx_SetIntegrationManager_Toggle"
|
</Form.Root>
|
||||||
checked={this.state.provisioningEnabled}
|
|
||||||
onChange={this.onProvisioningToggled}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Label htmlFor="mx_SetIntegrationManager_Toggle">
|
|
||||||
{_t("integration_manager|toggle_label")}
|
|
||||||
</Label>
|
|
||||||
</InlineField>
|
|
||||||
</Root>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, useState } from "react";
|
import React, { type JSX, useState } from "react";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import NewAndImprovedIcon from "../../../../../res/img/element-icons/new-and-improved.svg";
|
import NewAndImprovedIcon from "../../../../../res/img/element-icons/new-and-improved.svg";
|
||||||
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
|
||||||
import { useNotificationSettings } from "../../../../hooks/useNotificationSettings";
|
import { useNotificationSettings } from "../../../../hooks/useNotificationSettings";
|
||||||
import { useSettingValue } from "../../../../hooks/useSettings";
|
|
||||||
import { _t } from "../../../../languageHandler";
|
import { _t } from "../../../../languageHandler";
|
||||||
import {
|
import {
|
||||||
DefaultNotificationSettings,
|
DefaultNotificationSettings,
|
||||||
@@ -19,13 +19,11 @@ import {
|
|||||||
} from "../../../../models/notificationsettings/NotificationSettings";
|
} from "../../../../models/notificationsettings/NotificationSettings";
|
||||||
import { RoomNotifState } from "../../../../RoomNotifs";
|
import { RoomNotifState } from "../../../../RoomNotifs";
|
||||||
import { SettingLevel } from "../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../settings/SettingLevel";
|
||||||
import SettingsStore from "../../../../settings/SettingsStore";
|
|
||||||
import { NotificationLevel } from "../../../../stores/notifications/NotificationLevel";
|
import { NotificationLevel } from "../../../../stores/notifications/NotificationLevel";
|
||||||
import { clearAllNotifications } from "../../../../utils/notifications";
|
import { clearAllNotifications } from "../../../../utils/notifications";
|
||||||
import AccessibleButton from "../../elements/AccessibleButton";
|
import AccessibleButton from "../../elements/AccessibleButton";
|
||||||
import ExternalLink from "../../elements/ExternalLink";
|
import ExternalLink from "../../elements/ExternalLink";
|
||||||
import LabelledCheckbox from "../../elements/LabelledCheckbox";
|
import LabelledCheckbox from "../../elements/LabelledCheckbox";
|
||||||
import LabelledToggleSwitch from "../../elements/LabelledToggleSwitch";
|
|
||||||
import StyledRadioGroup from "../../elements/StyledRadioGroup";
|
import StyledRadioGroup from "../../elements/StyledRadioGroup";
|
||||||
import TagComposer from "../../elements/TagComposer";
|
import TagComposer from "../../elements/TagComposer";
|
||||||
import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge";
|
import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge";
|
||||||
@@ -70,10 +68,6 @@ function useHasUnreadNotifications(): boolean {
|
|||||||
export default function NotificationSettings2(): JSX.Element {
|
export default function NotificationSettings2(): JSX.Element {
|
||||||
const cli = useMatrixClientContext();
|
const cli = useMatrixClientContext();
|
||||||
|
|
||||||
const desktopNotifications = useSettingValue("notificationsEnabled");
|
|
||||||
const desktopShowBody = useSettingValue("notificationBodyEnabled");
|
|
||||||
const audioNotifications = useSettingValue("audioNotificationsEnabled");
|
|
||||||
|
|
||||||
const { model, hasPendingChanges, reconcile } = useNotificationSettings(cli);
|
const { model, hasPendingChanges, reconcile } = useNotificationSettings(cli);
|
||||||
|
|
||||||
const disabled = model === null || hasPendingChanges;
|
const disabled = model === null || hasPendingChanges;
|
||||||
@@ -116,267 +110,258 @@ export default function NotificationSettings2(): JSX.Element {
|
|||||||
</SettingsBanner>
|
</SettingsBanner>
|
||||||
)}
|
)}
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<div className="mx_SettingsSubsection_content mx_NotificationSettings2_flags">
|
<Form.Root
|
||||||
<LabelledToggleSwitch
|
onSubmit={(evt) => {
|
||||||
label={_t("settings|notifications|enable_notifications_account")}
|
evt.preventDefault();
|
||||||
value={!settings.globalMute}
|
evt.stopPropagation();
|
||||||
disabled={disabled}
|
}}
|
||||||
onChange={(value) => {
|
|
||||||
reconcile({
|
|
||||||
...model!,
|
|
||||||
globalMute: !value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
label={_t("settings|notifications|enable_desktop_notifications_session")}
|
|
||||||
value={desktopNotifications}
|
|
||||||
onChange={(value) =>
|
|
||||||
SettingsStore.setValue("notificationsEnabled", null, SettingLevel.DEVICE, value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
label={_t("settings|notifications|desktop_notification_message_preview")}
|
|
||||||
value={desktopShowBody}
|
|
||||||
onChange={(value) =>
|
|
||||||
SettingsStore.setValue("notificationBodyEnabled", null, SettingLevel.DEVICE, value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
label={_t("settings|notifications|enable_audible_notifications_session")}
|
|
||||||
value={audioNotifications}
|
|
||||||
onChange={(value) =>
|
|
||||||
SettingsStore.setValue("audioNotificationsEnabled", null, SettingLevel.DEVICE, value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<SettingsSubsection
|
|
||||||
heading={_t("settings|notifications|default_setting_section")}
|
|
||||||
description={_t("settings|notifications|default_setting_description")}
|
|
||||||
>
|
>
|
||||||
<StyledRadioGroup
|
<div className="mx_SettingsSubsection_content mx_NotificationSettings2_flags">
|
||||||
name="defaultNotificationLevel"
|
<SettingsToggleInput
|
||||||
value={toDefaultLevels(settings.defaultLevels)}
|
name="enable_notifications_account"
|
||||||
disabled={disabled}
|
label={_t("settings|notifications|enable_notifications_account")}
|
||||||
definitions={NotificationOptions}
|
checked={!settings.globalMute}
|
||||||
onChange={(value) => {
|
disabled={disabled}
|
||||||
reconcile({
|
onChange={(evt) => {
|
||||||
...model!,
|
reconcile({
|
||||||
defaultLevels: {
|
...model!,
|
||||||
...model!.defaultLevels,
|
globalMute: !evt.target.checked,
|
||||||
dm:
|
});
|
||||||
value !== NotificationDefaultLevels.MentionsKeywords
|
}}
|
||||||
? RoomNotifState.AllMessages
|
/>
|
||||||
: RoomNotifState.MentionsOnly,
|
<SettingsFlag name="notificationsEnabled" level={SettingLevel.DEVICE} />
|
||||||
room:
|
<SettingsFlag name="notificationBodyEnabled" level={SettingLevel.DEVICE} />
|
||||||
value === NotificationDefaultLevels.AllMessages
|
<SettingsFlag name="audioNotificationsEnabled" level={SettingLevel.DEVICE} />
|
||||||
? RoomNotifState.AllMessages
|
</div>
|
||||||
: RoomNotifState.MentionsOnly,
|
<SettingsSubsection
|
||||||
},
|
heading={_t("settings|notifications|default_setting_section")}
|
||||||
});
|
description={_t("settings|notifications|default_setting_description")}
|
||||||
}}
|
>
|
||||||
/>
|
<StyledRadioGroup
|
||||||
</SettingsSubsection>
|
name="defaultNotificationLevel"
|
||||||
<SettingsSubsection
|
value={toDefaultLevels(settings.defaultLevels)}
|
||||||
heading={_t("settings|notifications|play_sound_for_section")}
|
disabled={disabled}
|
||||||
description={_t("settings|notifications|play_sound_for_description")}
|
definitions={NotificationOptions}
|
||||||
>
|
onChange={(value) => {
|
||||||
<LabelledCheckbox
|
reconcile({
|
||||||
label={_t("common|people")}
|
...model!,
|
||||||
value={settings.sound.people !== undefined}
|
defaultLevels: {
|
||||||
disabled={disabled || settings.defaultLevels.dm === RoomNotifState.MentionsOnly}
|
...model!.defaultLevels,
|
||||||
onChange={(value) => {
|
dm:
|
||||||
reconcile({
|
value !== NotificationDefaultLevels.MentionsKeywords
|
||||||
...model!,
|
? RoomNotifState.AllMessages
|
||||||
sound: {
|
: RoomNotifState.MentionsOnly,
|
||||||
...model!.sound,
|
room:
|
||||||
people: value ? "default" : undefined,
|
value === NotificationDefaultLevels.AllMessages
|
||||||
},
|
? RoomNotifState.AllMessages
|
||||||
});
|
: RoomNotifState.MentionsOnly,
|
||||||
}}
|
},
|
||||||
/>
|
});
|
||||||
<LabelledCheckbox
|
}}
|
||||||
label={_t("settings|notifications|mentions_keywords")}
|
/>
|
||||||
value={settings.sound.mentions !== undefined}
|
</SettingsSubsection>
|
||||||
disabled={disabled}
|
<SettingsSubsection
|
||||||
onChange={(value) => {
|
heading={_t("settings|notifications|play_sound_for_section")}
|
||||||
reconcile({
|
description={_t("settings|notifications|play_sound_for_description")}
|
||||||
...model!,
|
>
|
||||||
sound: {
|
<LabelledCheckbox
|
||||||
...model!.sound,
|
label={_t("common|people")}
|
||||||
mentions: value ? "default" : undefined,
|
value={settings.sound.people !== undefined}
|
||||||
},
|
disabled={disabled || settings.defaultLevels.dm === RoomNotifState.MentionsOnly}
|
||||||
});
|
onChange={(value) => {
|
||||||
}}
|
reconcile({
|
||||||
/>
|
...model!,
|
||||||
<LabelledCheckbox
|
sound: {
|
||||||
label={_t("settings|notifications|voip")}
|
...model!.sound,
|
||||||
value={settings.sound.calls !== undefined}
|
people: value ? "default" : undefined,
|
||||||
disabled={disabled}
|
},
|
||||||
onChange={(value) => {
|
});
|
||||||
reconcile({
|
}}
|
||||||
...model!,
|
/>
|
||||||
sound: {
|
<LabelledCheckbox
|
||||||
...model!.sound,
|
label={_t("settings|notifications|mentions_keywords")}
|
||||||
calls: value ? "ring" : undefined,
|
value={settings.sound.mentions !== undefined}
|
||||||
},
|
disabled={disabled}
|
||||||
});
|
onChange={(value) => {
|
||||||
}}
|
reconcile({
|
||||||
/>
|
...model!,
|
||||||
</SettingsSubsection>
|
sound: {
|
||||||
<SettingsSubsection heading={_t("settings|notifications|other_section")}>
|
...model!.sound,
|
||||||
<LabelledCheckbox
|
mentions: value ? "default" : undefined,
|
||||||
label={_t("settings|notifications|invites")}
|
},
|
||||||
value={settings.activity.invite}
|
});
|
||||||
disabled={disabled}
|
}}
|
||||||
onChange={(value) => {
|
/>
|
||||||
reconcile({
|
<LabelledCheckbox
|
||||||
...model!,
|
label={_t("settings|notifications|voip")}
|
||||||
activity: {
|
value={settings.sound.calls !== undefined}
|
||||||
...model!.activity,
|
disabled={disabled}
|
||||||
invite: value,
|
onChange={(value) => {
|
||||||
},
|
reconcile({
|
||||||
});
|
...model!,
|
||||||
}}
|
sound: {
|
||||||
/>
|
...model!.sound,
|
||||||
<LabelledCheckbox
|
calls: value ? "ring" : undefined,
|
||||||
label={_t("settings|notifications|room_activity")}
|
},
|
||||||
value={settings.activity.status_event}
|
});
|
||||||
disabled={disabled}
|
}}
|
||||||
onChange={(value) => {
|
/>
|
||||||
reconcile({
|
</SettingsSubsection>
|
||||||
...model!,
|
<SettingsSubsection heading={_t("settings|notifications|other_section")}>
|
||||||
activity: {
|
<LabelledCheckbox
|
||||||
...model!.activity,
|
label={_t("settings|notifications|invites")}
|
||||||
status_event: value,
|
value={settings.activity.invite}
|
||||||
},
|
disabled={disabled}
|
||||||
});
|
onChange={(value) => {
|
||||||
}}
|
reconcile({
|
||||||
/>
|
...model!,
|
||||||
<LabelledCheckbox
|
activity: {
|
||||||
label={_t("settings|notifications|notices")}
|
...model!.activity,
|
||||||
value={settings.activity.bot_notices}
|
invite: value,
|
||||||
disabled={disabled}
|
},
|
||||||
onChange={(value) => {
|
});
|
||||||
reconcile({
|
}}
|
||||||
...model!,
|
/>
|
||||||
activity: {
|
<LabelledCheckbox
|
||||||
...model!.activity,
|
label={_t("settings|notifications|room_activity")}
|
||||||
bot_notices: value,
|
value={settings.activity.status_event}
|
||||||
},
|
disabled={disabled}
|
||||||
});
|
onChange={(value) => {
|
||||||
}}
|
reconcile({
|
||||||
/>
|
...model!,
|
||||||
</SettingsSubsection>
|
activity: {
|
||||||
<SettingsSubsection
|
...model!.activity,
|
||||||
heading={_t("settings|notifications|mentions_keywords")}
|
status_event: value,
|
||||||
description={_t(
|
},
|
||||||
"settings|notifications|keywords",
|
});
|
||||||
{},
|
}}
|
||||||
{
|
/>
|
||||||
badge: (
|
<LabelledCheckbox
|
||||||
<StatelessNotificationBadge
|
label={_t("settings|notifications|notices")}
|
||||||
symbol="1"
|
value={settings.activity.bot_notices}
|
||||||
count={1}
|
disabled={disabled}
|
||||||
level={NotificationLevel.Notification}
|
onChange={(value) => {
|
||||||
/>
|
reconcile({
|
||||||
),
|
...model!,
|
||||||
},
|
activity: {
|
||||||
)}
|
...model!.activity,
|
||||||
>
|
bot_notices: value,
|
||||||
<LabelledCheckbox
|
},
|
||||||
label={_t("settings|notifications|notify_at_room")}
|
});
|
||||||
value={settings.mentions.room}
|
}}
|
||||||
disabled={disabled}
|
/>
|
||||||
onChange={(value) => {
|
</SettingsSubsection>
|
||||||
reconcile({
|
<SettingsSubsection
|
||||||
...model!,
|
heading={_t("settings|notifications|mentions_keywords")}
|
||||||
mentions: {
|
description={_t(
|
||||||
...model!.mentions,
|
"settings|notifications|keywords",
|
||||||
room: value,
|
{},
|
||||||
},
|
{
|
||||||
});
|
badge: (
|
||||||
}}
|
<StatelessNotificationBadge
|
||||||
/>
|
symbol="1"
|
||||||
<LabelledCheckbox
|
count={1}
|
||||||
label={_t("settings|notifications|notify_mention", {
|
level={NotificationLevel.Notification}
|
||||||
mxid: cli.getUserId()!,
|
/>
|
||||||
})}
|
),
|
||||||
value={settings.mentions.user}
|
},
|
||||||
disabled={disabled}
|
)}
|
||||||
onChange={(value) => {
|
>
|
||||||
reconcile({
|
<LabelledCheckbox
|
||||||
...model!,
|
label={_t("settings|notifications|notify_at_room")}
|
||||||
mentions: {
|
value={settings.mentions.room}
|
||||||
...model!.mentions,
|
disabled={disabled}
|
||||||
user: value,
|
onChange={(value) => {
|
||||||
},
|
reconcile({
|
||||||
});
|
...model!,
|
||||||
}}
|
mentions: {
|
||||||
/>
|
...model!.mentions,
|
||||||
<LabelledCheckbox
|
room: value,
|
||||||
label={_t("settings|notifications|notify_keyword")}
|
},
|
||||||
byline={_t("settings|notifications|keywords_prompt")}
|
});
|
||||||
value={settings.mentions.keywords}
|
}}
|
||||||
disabled={disabled}
|
/>
|
||||||
onChange={(value) => {
|
<LabelledCheckbox
|
||||||
reconcile({
|
label={_t("settings|notifications|notify_mention", {
|
||||||
...model!,
|
mxid: cli.getUserId()!,
|
||||||
mentions: {
|
})}
|
||||||
...model!.mentions,
|
id="mx_NotificationSettings2_MentionCheckbox"
|
||||||
keywords: value,
|
value={settings.mentions.user}
|
||||||
},
|
disabled={disabled}
|
||||||
});
|
onChange={(value) => {
|
||||||
}}
|
reconcile({
|
||||||
/>
|
...model!,
|
||||||
<TagComposer
|
mentions: {
|
||||||
id="mx_NotificationSettings2_Keywords"
|
...model!.mentions,
|
||||||
tags={model?.keywords ?? []}
|
user: value,
|
||||||
disabled={disabled}
|
},
|
||||||
onAdd={(keyword) => {
|
});
|
||||||
reconcile({
|
}}
|
||||||
...model!,
|
/>
|
||||||
keywords: [keyword, ...model!.keywords],
|
<LabelledCheckbox
|
||||||
});
|
label={_t("settings|notifications|notify_keyword")}
|
||||||
}}
|
byline={_t("settings|notifications|keywords_prompt")}
|
||||||
onRemove={(keyword) => {
|
value={settings.mentions.keywords}
|
||||||
reconcile({
|
disabled={disabled}
|
||||||
...model!,
|
onChange={(value) => {
|
||||||
keywords: model!.keywords.filter((it) => it !== keyword),
|
reconcile({
|
||||||
});
|
...model!,
|
||||||
}}
|
mentions: {
|
||||||
label={_t("notifications|keyword")}
|
...model!.mentions,
|
||||||
placeholder={_t("notifications|keyword_new")}
|
keywords: value,
|
||||||
/>
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TagComposer
|
||||||
|
id="mx_NotificationSettings2_Keywords"
|
||||||
|
tags={model?.keywords ?? []}
|
||||||
|
disabled={disabled}
|
||||||
|
onAdd={(keyword) => {
|
||||||
|
reconcile({
|
||||||
|
...model!,
|
||||||
|
keywords: [keyword, ...model!.keywords],
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onRemove={(keyword) => {
|
||||||
|
reconcile({
|
||||||
|
...model!,
|
||||||
|
keywords: model!.keywords.filter((it) => it !== keyword),
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
label={_t("notifications|keyword")}
|
||||||
|
placeholder={_t("notifications|keyword_new")}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingsFlag name="Notifications.showbold" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="Notifications.showbold" level={SettingLevel.DEVICE} />
|
||||||
<SettingsFlag name="Notifications.tac_only_notifications" level={SettingLevel.DEVICE} />
|
<SettingsFlag name="Notifications.tac_only_notifications" level={SettingLevel.DEVICE} />
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
<NotificationPusherSettings />
|
<NotificationPusherSettings />
|
||||||
<SettingsSubsection heading={_t("settings|notifications|quick_actions_section")}>
|
<SettingsSubsection heading={_t("settings|notifications|quick_actions_section")}>
|
||||||
{hasUnreadNotifications && (
|
{hasUnreadNotifications && (
|
||||||
|
<AccessibleButton
|
||||||
|
kind="primary_outline"
|
||||||
|
disabled={updatingUnread}
|
||||||
|
onClick={async () => {
|
||||||
|
setUpdatingUnread(true);
|
||||||
|
await clearAllNotifications(cli);
|
||||||
|
setUpdatingUnread(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{_t("settings|notifications|quick_actions_mark_all_read")}
|
||||||
|
</AccessibleButton>
|
||||||
|
)}
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="primary_outline"
|
kind="danger_outline"
|
||||||
disabled={updatingUnread}
|
disabled={model === null}
|
||||||
onClick={async () => {
|
onClick={() => {
|
||||||
setUpdatingUnread(true);
|
reconcile(DefaultNotificationSettings);
|
||||||
await clearAllNotifications(cli);
|
|
||||||
setUpdatingUnread(false);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{_t("settings|notifications|quick_actions_mark_all_read")}
|
{_t("settings|notifications|quick_actions_reset")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
)}
|
</SettingsSubsection>
|
||||||
<AccessibleButton
|
</Form.Root>
|
||||||
kind="danger_outline"
|
|
||||||
disabled={model === null}
|
|
||||||
onClick={() => {
|
|
||||||
reconcile(DefaultNotificationSettings);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{_t("settings|notifications|quick_actions_reset")}
|
|
||||||
</AccessibleButton>
|
|
||||||
</SettingsSubsection>
|
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React, { type HTMLAttributes } from "react";
|
import React, { type HTMLAttributes } from "react";
|
||||||
import { Separator } from "@vector-im/compound-web";
|
import { Form, Separator } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading";
|
import { SettingsSubsectionHeading } from "./SettingsSubsectionHeading";
|
||||||
|
|
||||||
@@ -23,6 +23,11 @@ export interface SettingsSubsectionProps extends HTMLAttributes<HTMLDivElement>
|
|||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
legacy?: boolean;
|
legacy?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrap in a Form Root component, for compatibility with compound components.
|
||||||
|
*/
|
||||||
|
formWrap?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingsSubsectionText: React.FC<HTMLAttributes<HTMLDivElement>> = ({ children, ...rest }) => (
|
export const SettingsSubsectionText: React.FC<HTMLAttributes<HTMLDivElement>> = ({ children, ...rest }) => (
|
||||||
@@ -37,31 +42,48 @@ export const SettingsSubsection: React.FC<SettingsSubsectionProps> = ({
|
|||||||
children,
|
children,
|
||||||
stretchContent,
|
stretchContent,
|
||||||
legacy = true,
|
legacy = true,
|
||||||
|
formWrap,
|
||||||
...rest
|
...rest
|
||||||
}) => (
|
}) => {
|
||||||
<div
|
const content = (
|
||||||
{...rest}
|
<div
|
||||||
className={classNames("mx_SettingsSubsection", {
|
{...rest}
|
||||||
mx_SettingsSubsection_newUi: !legacy,
|
className={classNames("mx_SettingsSubsection", {
|
||||||
})}
|
mx_SettingsSubsection_newUi: !legacy,
|
||||||
>
|
})}
|
||||||
{typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} /> : <>{heading}</>}
|
>
|
||||||
{!!description && (
|
{typeof heading === "string" ? <SettingsSubsectionHeading heading={heading} /> : <>{heading}</>}
|
||||||
<div className="mx_SettingsSubsection_description">
|
{!!description && (
|
||||||
<SettingsSubsectionText>{description}</SettingsSubsectionText>
|
<div className="mx_SettingsSubsection_description">
|
||||||
</div>
|
<SettingsSubsectionText>{description}</SettingsSubsectionText>
|
||||||
)}
|
</div>
|
||||||
{!!children && (
|
)}
|
||||||
<div
|
{!!children && (
|
||||||
className={classNames("mx_SettingsSubsection_content", {
|
<div
|
||||||
mx_SettingsSubsection_contentStretch: !!stretchContent,
|
className={classNames("mx_SettingsSubsection_content", {
|
||||||
mx_SettingsSubsection_noHeading: !heading && !description,
|
mx_SettingsSubsection_contentStretch: !!stretchContent,
|
||||||
mx_SettingsSubsection_content_newUi: !legacy,
|
mx_SettingsSubsection_noHeading: !heading && !description,
|
||||||
})}
|
mx_SettingsSubsection_content_newUi: !legacy,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!legacy && <Separator />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (formWrap) {
|
||||||
|
return (
|
||||||
|
<Form.Root
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{content}
|
||||||
</div>
|
</Form.Root>
|
||||||
)}
|
);
|
||||||
{!legacy && <Separator />}
|
}
|
||||||
</div>
|
return content;
|
||||||
);
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type ContextType } from "react";
|
import React, { type ContextType } from "react";
|
||||||
import { type Room } from "matrix-js-sdk/src/matrix";
|
import { type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
|
import { Form } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
import RoomProfileSettings from "../../../room_settings/RoomProfileSettings";
|
||||||
@@ -78,26 +79,33 @@ export default class GeneralRoomSettingsTab extends React.Component<IProps, ISta
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab data-testid="General">
|
<SettingsTab data-testid="General">
|
||||||
<SettingsSection heading={_t("common|general")}>
|
<Form.Root
|
||||||
<RoomProfileSettings roomId={room.roomId} />
|
onSubmit={(evt) => {
|
||||||
</SettingsSection>
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingsSection heading={_t("common|general")}>
|
||||||
|
<RoomProfileSettings roomId={room.roomId} />
|
||||||
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection heading={_t("room_settings|general|aliases_section")}>
|
<SettingsSection heading={_t("room_settings|general|aliases_section")}>
|
||||||
<AliasSettings
|
<AliasSettings
|
||||||
roomId={room.roomId}
|
roomId={room.roomId}
|
||||||
canSetCanonicalAlias={canSetCanonical}
|
canSetCanonicalAlias={canSetCanonical}
|
||||||
canSetAliases={canSetAliases}
|
canSetAliases={canSetAliases}
|
||||||
canonicalAliasEvent={canonicalAliasEv}
|
canonicalAliasEvent={canonicalAliasEv}
|
||||||
/>
|
/>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
|
||||||
<SettingsSection heading={_t("room_settings|general|other_section")}>
|
<SettingsSection heading={_t("room_settings|general|other_section")}>
|
||||||
{urlPreviewSettings}
|
{urlPreviewSettings}
|
||||||
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||||
<MediaPreviewAccountSettings roomId={room.roomId} />
|
<MediaPreviewAccountSettings roomId={room.roomId} />
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
{leaveSection}
|
{leaveSection}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
|
</Form.Root>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ReactNode } from "react";
|
import React, { type ChangeEventHandler, type JSX, type ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
GuestAccess,
|
GuestAccess,
|
||||||
HistoryVisibility,
|
HistoryVisibility,
|
||||||
@@ -17,11 +17,10 @@ import {
|
|||||||
EventType,
|
EventType,
|
||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { InlineSpinner } from "@vector-im/compound-web";
|
import { Form, InlineSpinner, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg";
|
import { Icon as WarningIcon } from "../../../../../../res/img/warning.svg";
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
import QuestionDialog from "../../../dialogs/QuestionDialog";
|
||||||
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
import StyledRadioGroup from "../../../elements/StyledRadioGroup";
|
||||||
@@ -184,7 +183,8 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onGuestAccessChange = (allowed: boolean): void => {
|
private onGuestAccessChange: ChangeEventHandler<HTMLInputElement> = (evt): void => {
|
||||||
|
const allowed = evt.target.checked;
|
||||||
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
|
const guestAccess = allowed ? GuestAccess.CanJoin : GuestAccess.Forbidden;
|
||||||
const beforeGuestAccess = this.state.guestAccess;
|
const beforeGuestAccess = this.state.guestAccess;
|
||||||
if (beforeGuestAccess === guestAccess) return;
|
if (beforeGuestAccess === guestAccess) return;
|
||||||
@@ -405,13 +405,14 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_SecurityRoomSettingsTab_advancedSection">
|
<div className="mx_SecurityRoomSettingsTab_advancedSection">
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
value={guestAccess === GuestAccess.CanJoin}
|
name="guest-access"
|
||||||
|
checked={guestAccess === GuestAccess.CanJoin}
|
||||||
onChange={this.onGuestAccessChange}
|
onChange={this.onGuestAccessChange}
|
||||||
disabled={!canSetGuestAccess}
|
disabled={!canSetGuestAccess}
|
||||||
label={_t("room_settings|visibility|guest_access_label")}
|
label={_t("room_settings|visibility|guest_access_label")}
|
||||||
|
helpMessage={_t("room_settings|security|guest_access_warning")}
|
||||||
/>
|
/>
|
||||||
<p>{_t("room_settings|security|guest_access_warning")}</p>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -445,33 +446,41 @@ export default class SecurityRoomSettingsTab extends React.Component<IProps, ISt
|
|||||||
return (
|
return (
|
||||||
<SettingsTab>
|
<SettingsTab>
|
||||||
<SettingsSection heading={_t("room_settings|security|title")}>
|
<SettingsSection heading={_t("room_settings|security|title")}>
|
||||||
<SettingsFieldset
|
<Form.Root
|
||||||
legend={_t("settings|security|encryption_section")}
|
onSubmit={(evt) => {
|
||||||
description={
|
evt.preventDefault();
|
||||||
isEncryptionForceDisabled && !isEncrypted
|
evt.stopPropagation();
|
||||||
? undefined
|
}}
|
||||||
: _t("room_settings|security|encryption_permanent")
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{isEncryptionLoading ? (
|
<SettingsFieldset
|
||||||
<InlineSpinner />
|
legend={_t("settings|security|encryption_section")}
|
||||||
) : (
|
description={
|
||||||
<>
|
isEncryptionForceDisabled && !isEncrypted
|
||||||
<LabelledToggleSwitch
|
? undefined
|
||||||
value={isEncrypted}
|
: _t("room_settings|security|encryption_permanent")
|
||||||
onChange={this.onEncryptionChange}
|
}
|
||||||
label={_t("common|encrypted")}
|
>
|
||||||
disabled={!canEnableEncryption}
|
{isEncryptionLoading ? (
|
||||||
/>
|
<InlineSpinner />
|
||||||
{isEncryptionForceDisabled && !isEncrypted && (
|
) : (
|
||||||
<Caption>{_t("room_settings|security|encryption_forced")}</Caption>
|
<>
|
||||||
)}
|
<SettingsToggleInput
|
||||||
{encryptionSettings}
|
name="enable-encryption"
|
||||||
</>
|
checked={isEncrypted}
|
||||||
)}
|
onChange={this.onEncryptionChange}
|
||||||
</SettingsFieldset>
|
label={_t("common|encrypted")}
|
||||||
{this.renderJoinRule()}
|
disabled={!canEnableEncryption}
|
||||||
{historySection}
|
/>
|
||||||
|
{isEncryptionForceDisabled && !isEncrypted && (
|
||||||
|
<Caption>{_t("room_settings|security|encryption_forced")}</Caption>
|
||||||
|
)}
|
||||||
|
{encryptionSettings}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</SettingsFieldset>
|
||||||
|
{this.renderJoinRule()}
|
||||||
|
{historySection}
|
||||||
|
</Form.Root>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
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, useMemo, useState } from "react";
|
import React, { type ChangeEventHandler, useCallback, useMemo, useState } from "react";
|
||||||
import { JoinRule, EventType, type RoomState, type Room } from "matrix-js-sdk/src/matrix";
|
import { JoinRule, EventType, type RoomState, type Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { type RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
|
import { type RoomPowerLevelsEventContent } from "matrix-js-sdk/src/types";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|
||||||
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
import { SettingsSubsection } from "../../shared/SettingsSubsection";
|
||||||
import SettingsTab from "../SettingsTab";
|
import SettingsTab from "../SettingsTab";
|
||||||
import { ElementCall } from "../../../../../models/Call";
|
import { ElementCall } from "../../../../../models/Call";
|
||||||
@@ -45,8 +45,9 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
|
|||||||
return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
|
return content.events?.[ElementCall.MEMBER_EVENT_TYPE.name] === 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onChange = useCallback(
|
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
(enabled: boolean): void => {
|
(evt): void => {
|
||||||
|
const enabled = evt.target.checked;
|
||||||
setElementCallEnabled(enabled);
|
setElementCallEnabled(enabled);
|
||||||
|
|
||||||
// Take a copy to avoid mutating the original
|
// Take a copy to avoid mutating the original
|
||||||
@@ -73,16 +74,17 @@ const ElementCallSwitch: React.FC<ElementCallSwitchProps> = ({ room }) => {
|
|||||||
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
const brand = SdkConfig.get("element_call").brand ?? DEFAULTS.element_call.brand;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
data-testid="element-call-switch"
|
name="element-call-switch"
|
||||||
|
data-test-id="element-call-switch"
|
||||||
label={_t("room_settings|voip|enable_element_call_label", { brand })}
|
label={_t("room_settings|voip|enable_element_call_label", { brand })}
|
||||||
caption={_t("room_settings|voip|enable_element_call_caption", {
|
helpMessage={_t("room_settings|voip|enable_element_call_caption", {
|
||||||
brand,
|
brand,
|
||||||
})}
|
})}
|
||||||
value={elementCallEnabled}
|
checked={elementCallEnabled}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
disabled={!maySend}
|
disabled={!maySend}
|
||||||
tooltip={_t("room_settings|voip|enable_element_call_no_permissions_tooltip")}
|
disabledMessage={_t("room_settings|voip|enable_element_call_no_permissions_tooltip")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -95,9 +97,16 @@ export const VoipRoomSettingsTab: React.FC<Props> = ({ room }) => {
|
|||||||
return (
|
return (
|
||||||
<SettingsTab>
|
<SettingsTab>
|
||||||
<SettingsSection heading={_t("settings|voip|title")}>
|
<SettingsSection heading={_t("settings|voip|title")}>
|
||||||
<SettingsSubsection heading={_t("room_settings|voip|call_type_section")}>
|
<Form.Root
|
||||||
<ElementCallSwitch room={room} />
|
onSubmit={(evt) => {
|
||||||
</SettingsSubsection>
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingsSubsection heading={_t("room_settings|voip|call_type_section")}>
|
||||||
|
<ElementCallSwitch room={room} />
|
||||||
|
</SettingsSubsection>
|
||||||
|
</Form.Root>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
|
|
||||||
import React, { type ChangeEvent, type ReactNode } from "react";
|
import React, { type ChangeEvent, type ReactNode } from "react";
|
||||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Form } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||||
import Field from "../../../elements/Field";
|
import Field from "../../../elements/Field";
|
||||||
@@ -48,7 +48,6 @@ export default class AppearanceUserSettingsTab extends React.Component<EmptyObje
|
|||||||
private renderAdvancedSection(): ReactNode {
|
private renderAdvancedSection(): ReactNode {
|
||||||
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
if (!SettingsStore.getValue(UIFeature.AdvancedSettings)) return null;
|
||||||
|
|
||||||
const brand = SdkConfig.get().brand;
|
|
||||||
const toggle = (
|
const toggle = (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
kind="link"
|
kind="link"
|
||||||
@@ -62,21 +61,18 @@ export default class AppearanceUserSettingsTab extends React.Component<EmptyObje
|
|||||||
let advanced: React.ReactNode;
|
let advanced: React.ReactNode;
|
||||||
|
|
||||||
if (this.state.showAdvanced) {
|
if (this.state.showAdvanced) {
|
||||||
const tooltipContent = _t("settings|appearance|custom_font_description", { brand });
|
|
||||||
advanced = (
|
advanced = (
|
||||||
<>
|
<>
|
||||||
<SettingsFlag name="useCompactLayout" level={SettingLevel.DEVICE} useCheckbox={true} />
|
<SettingsFlag name="useCompactLayout" level={SettingLevel.DEVICE} />
|
||||||
|
|
||||||
<SettingsFlag
|
<SettingsFlag
|
||||||
name="useBundledEmojiFont"
|
name="useBundledEmojiFont"
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
useCheckbox={true}
|
|
||||||
onChange={(checked) => this.setState({ useBundledEmojiFont: checked })}
|
onChange={(checked) => this.setState({ useBundledEmojiFont: checked })}
|
||||||
/>
|
/>
|
||||||
<SettingsFlag
|
<SettingsFlag
|
||||||
name="useSystemFont"
|
name="useSystemFont"
|
||||||
level={SettingLevel.DEVICE}
|
level={SettingLevel.DEVICE}
|
||||||
useCheckbox={true}
|
|
||||||
onChange={(checked) => this.setState({ useSystemFont: checked })}
|
onChange={(checked) => this.setState({ useSystemFont: checked })}
|
||||||
/>
|
/>
|
||||||
<Field
|
<Field
|
||||||
@@ -89,8 +85,6 @@ export default class AppearanceUserSettingsTab extends React.Component<EmptyObje
|
|||||||
|
|
||||||
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
|
SettingsStore.setValue("systemFont", null, SettingLevel.DEVICE, value.target.value);
|
||||||
}}
|
}}
|
||||||
tooltipContent={tooltipContent}
|
|
||||||
forceTooltipVisible={true}
|
|
||||||
disabled={!this.state.useSystemFont}
|
disabled={!this.state.useSystemFont}
|
||||||
value={this.state.systemFont}
|
value={this.state.systemFont}
|
||||||
/>
|
/>
|
||||||
@@ -109,11 +103,18 @@ export default class AppearanceUserSettingsTab extends React.Component<EmptyObje
|
|||||||
return (
|
return (
|
||||||
<SettingsTab data-testid="mx_AppearanceUserSettingsTab">
|
<SettingsTab data-testid="mx_AppearanceUserSettingsTab">
|
||||||
<SettingsSection>
|
<SettingsSection>
|
||||||
<ThemeChoicePanel />
|
<Form.Root
|
||||||
<LayoutSwitcher />
|
onSubmit={(evt) => {
|
||||||
<FontScalingPanel />
|
evt.preventDefault();
|
||||||
{this.renderAdvancedSection()}
|
evt.stopPropagation();
|
||||||
<ImageSizePanel />
|
}}
|
||||||
|
>
|
||||||
|
<ThemeChoicePanel />
|
||||||
|
<LayoutSwitcher />
|
||||||
|
<FontScalingPanel />
|
||||||
|
{this.renderAdvancedSection()}
|
||||||
|
<ImageSizePanel />
|
||||||
|
</Form.Root>
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,26 +5,25 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type FC, useCallback, useState } from "react";
|
import React, { type ChangeEventHandler, type FC, useCallback, useState } from "react";
|
||||||
import { Root } from "@vector-im/compound-web";
|
import { Root, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|
||||||
|
|
||||||
export const InviteRulesAccountSetting: FC = () => {
|
export const InviteRulesAccountSetting: FC = () => {
|
||||||
const rules = useSettingValue("inviteRules");
|
const rules = useSettingValue("inviteRules");
|
||||||
const settingsDisabled = SettingsStore.disabledMessage("inviteRules");
|
const settingsDisabled = SettingsStore.disabledMessage("inviteRules");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const onChange = useCallback(async (checked: boolean) => {
|
const onChange = useCallback<ChangeEventHandler<HTMLInputElement>>(async (evt) => {
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
await SettingsStore.setValue("inviteRules", null, SettingLevel.ACCOUNT, {
|
await SettingsStore.setValue("inviteRules", null, SettingLevel.ACCOUNT, {
|
||||||
allBlocked: !checked,
|
allBlocked: !evt.target.checked,
|
||||||
});
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
logger.error(`Unable to set invite rules`, ex);
|
logger.error(`Unable to set invite rules`, ex);
|
||||||
@@ -33,13 +32,20 @@ export const InviteRulesAccountSetting: FC = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
return (
|
return (
|
||||||
<Root className="mx_MediaPreviewAccountSetting_Form">
|
<Root
|
||||||
<LabelledToggleSwitch
|
className="mx_MediaPreviewAccountSetting_Form"
|
||||||
|
onSubmit={(evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SettingsToggleInput
|
||||||
className="mx_MediaPreviewAccountSetting_ToggleSwitch"
|
className="mx_MediaPreviewAccountSetting_ToggleSwitch"
|
||||||
|
name="invite_control_blocked"
|
||||||
label={_t("settings|invite_controls|default_label")}
|
label={_t("settings|invite_controls|default_label")}
|
||||||
value={!rules.allBlocked}
|
checked={!rules.allBlocked}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
tooltip={settingsDisabled}
|
disabledMessage={settingsDisabled}
|
||||||
disabled={!!settingsDisabled || busy}
|
disabled={!!settingsDisabled || busy}
|
||||||
/>
|
/>
|
||||||
</Root>
|
</Root>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type JSX } from "react";
|
import React, { type JSX } from "react";
|
||||||
import { sortBy } from "lodash";
|
import { sortBy } from "lodash";
|
||||||
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
import { type EmptyObject } from "matrix-js-sdk/src/matrix";
|
||||||
|
import { Form } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import SettingsStore from "../../../../../settings/SettingsStore";
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
@@ -106,37 +107,44 @@ export default class LabsUserSettingsTab extends React.Component<EmptyObject> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsTab>
|
<SettingsTab>
|
||||||
<SettingsSection heading={_t("labs|beta_section")}>
|
<Form.Root
|
||||||
<SettingsSubsectionText>
|
onSubmit={(evt) => {
|
||||||
{_t("labs|beta_description", { brand: SdkConfig.get("brand") })}
|
evt.preventDefault();
|
||||||
</SettingsSubsectionText>
|
evt.stopPropagation();
|
||||||
{betaSection}
|
}}
|
||||||
</SettingsSection>
|
>
|
||||||
|
<SettingsSection heading={_t("labs|beta_section")}>
|
||||||
{labsSections && (
|
|
||||||
<SettingsSection heading={_t("labs|experimental_section")}>
|
|
||||||
<SettingsSubsectionText>
|
<SettingsSubsectionText>
|
||||||
{_t(
|
{_t("labs|beta_description", { brand: SdkConfig.get("brand") })}
|
||||||
"labs|experimental_description",
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
a: (sub) => {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
|
||||||
rel="noreferrer noopener"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
{sub}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
)}
|
|
||||||
</SettingsSubsectionText>
|
</SettingsSubsectionText>
|
||||||
{labsSections}
|
{betaSection}
|
||||||
</SettingsSection>
|
</SettingsSection>
|
||||||
)}
|
|
||||||
|
{labsSections && (
|
||||||
|
<SettingsSection heading={_t("labs|experimental_section")}>
|
||||||
|
<SettingsSubsectionText>
|
||||||
|
{_t(
|
||||||
|
"labs|experimental_description",
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
a: (sub) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href="https://github.com/vector-im/element-web/blob/develop/docs/labs.md"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{sub}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</SettingsSubsectionText>
|
||||||
|
{labsSections}
|
||||||
|
</SettingsSection>
|
||||||
|
)}
|
||||||
|
</Form.Root>
|
||||||
</SettingsTab>
|
</SettingsTab>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,8 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type ChangeEventHandler, useCallback } from "react";
|
import React, { type ChangeEventHandler, useCallback } from "react";
|
||||||
import { Field, HelpMessage, InlineField, Label, RadioInput, Root } from "@vector-im/compound-web";
|
import { Field, HelpMessage, InlineField, Label, RadioInput, Root, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|
||||||
import { type MediaPreviewConfig, MediaPreviewValue } from "../../../../../@types/media_preview";
|
import { type MediaPreviewConfig, MediaPreviewValue } from "../../../../../@types/media_preview";
|
||||||
import { _t } from "../../../../../languageHandler";
|
import { _t } from "../../../../../languageHandler";
|
||||||
import { useSettingValue } from "../../../../../hooks/useSettings";
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
@@ -30,12 +29,12 @@ export const MediaPreviewAccountSettings: React.FC<{ roomId?: string }> = ({ roo
|
|||||||
[roomId],
|
[roomId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const avatarOnChange = useCallback(
|
const avatarOnChange = useCallback<ChangeEventHandler<HTMLInputElement>>(
|
||||||
(c: boolean) => {
|
(evt) => {
|
||||||
changeSetting({
|
changeSetting({
|
||||||
...currentMediaPreview,
|
...currentMediaPreview,
|
||||||
// Switch is inverted. "Hide avatars..."
|
// Switch is inverted. "Hide avatars..."
|
||||||
invite_avatars: c ? MediaPreviewValue.Off : MediaPreviewValue.On,
|
invite_avatars: evt.target.checked ? MediaPreviewValue.Off : MediaPreviewValue.On,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[changeSetting, currentMediaPreview],
|
[changeSetting, currentMediaPreview],
|
||||||
@@ -83,10 +82,10 @@ export const MediaPreviewAccountSettings: React.FC<{ roomId?: string }> = ({ roo
|
|||||||
return (
|
return (
|
||||||
<Root className="mx_MediaPreviewAccountSetting_Form">
|
<Root className="mx_MediaPreviewAccountSetting_Form">
|
||||||
{!roomId && (
|
{!roomId && (
|
||||||
<LabelledToggleSwitch
|
<SettingsToggleInput
|
||||||
className="mx_MediaPreviewAccountSetting_ToggleSwitch"
|
name="hide_avatars"
|
||||||
label={_t("settings|media_preview|hide_avatars")}
|
label={_t("settings|media_preview|hide_avatars")}
|
||||||
value={currentMediaPreview.invite_avatars === MediaPreviewValue.Off}
|
checked={currentMediaPreview.invite_avatars === MediaPreviewValue.Off}
|
||||||
onChange={avatarOnChange}
|
onChange={avatarOnChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||||||
Please see LICENSE files in the repository root for full details.
|
Please see LICENSE files in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { type JSX, type ReactElement, useCallback, useEffect, useState } from "react";
|
import React, { type ChangeEventHandler, type JSX, type ReactElement, useCallback, useEffect, useState } from "react";
|
||||||
|
import { Form, SettingsToggleInput } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import { type NonEmptyArray } from "../../../../../@types/common";
|
import { type NonEmptyArray } from "../../../../../@types/common";
|
||||||
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
import { _t, getCurrentLanguage } from "../../../../../languageHandler";
|
||||||
@@ -29,7 +30,6 @@ import LanguageDropdown from "../../../elements/LanguageDropdown";
|
|||||||
import PlatformPeg from "../../../../../PlatformPeg";
|
import PlatformPeg from "../../../../../PlatformPeg";
|
||||||
import { IS_MAC } from "../../../../../Keyboard";
|
import { IS_MAC } from "../../../../../Keyboard";
|
||||||
import SpellCheckSettings from "../../SpellCheckSettings";
|
import SpellCheckSettings from "../../SpellCheckSettings";
|
||||||
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|
||||||
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
||||||
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
||||||
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
|
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
|
||||||
@@ -91,9 +91,9 @@ const SpellCheckSection: React.FC = () => {
|
|||||||
})();
|
})();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSpellCheckEnabledChange = useCallback((enabled: boolean) => {
|
const onSpellCheckEnabledChange = useCallback<ChangeEventHandler<HTMLInputElement>>((evt) => {
|
||||||
setSpellCheckEnabled(enabled);
|
setSpellCheckEnabled(evt.target.checked);
|
||||||
PlatformPeg.get()?.setSpellCheckEnabled(enabled);
|
PlatformPeg.get()?.setSpellCheckEnabled(evt.target.checked);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSpellCheckLanguagesChange = useCallback((languages: string[]): void => {
|
const onSpellCheckLanguagesChange = useCallback((languages: string[]): void => {
|
||||||
@@ -105,11 +105,14 @@ const SpellCheckSection: React.FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LabelledToggleSwitch
|
<Form.Root>
|
||||||
label={_t("settings|general|allow_spellcheck")}
|
<SettingsToggleInput
|
||||||
value={Boolean(spellCheckEnabled)}
|
label={_t("settings|general|allow_spellcheck")}
|
||||||
onChange={onSpellCheckEnabledChange}
|
name="spell_check"
|
||||||
/>
|
checked={Boolean(spellCheckEnabled)}
|
||||||
|
onChange={onSpellCheckEnabledChange}
|
||||||
|
/>
|
||||||
|
</Form.Root>
|
||||||
{spellCheckEnabled && spellCheckLanguages !== undefined && !IS_MAC && (
|
{spellCheckEnabled && spellCheckLanguages !== undefined && !IS_MAC && (
|
||||||
<SpellCheckSettings languages={spellCheckLanguages} onLanguagesChange={onSpellCheckLanguagesChange} />
|
<SpellCheckSettings languages={spellCheckLanguages} onLanguagesChange={onSpellCheckLanguagesChange} />
|
||||||
)}
|
)}
|
||||||
@@ -263,8 +266,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
<LanguageSection />
|
<LanguageSection />
|
||||||
<SpellCheckSection />
|
<SpellCheckSection />
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
<SettingsSubsection formWrap heading={_t("settings|preferences|room_list_heading")}>
|
||||||
<SettingsSubsection heading={_t("settings|preferences|room_list_heading")}>
|
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.ROOM_LIST_SETTINGS)}
|
||||||
{/* The settings is on device level where the other room list settings are on account level */}
|
{/* The settings is on device level where the other room list settings are on account level */}
|
||||||
{newRoomListEnabled && (
|
{newRoomListEnabled && (
|
||||||
@@ -272,12 +274,13 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
)}
|
)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("common|spaces")}>
|
<SettingsSubsection formWrap heading={_t("common|spaces")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT)}
|
{this.renderGroup(PreferencesUserSettingsTab.SPACES_SETTINGS, SettingLevel.ACCOUNT)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection
|
<SettingsSubsection
|
||||||
heading={_t("settings|preferences|keyboard_heading")}
|
heading={_t("settings|preferences|keyboard_heading")}
|
||||||
|
formWrap
|
||||||
description={_t(
|
description={_t(
|
||||||
"settings|preferences|keyboard_view_shortcuts_button",
|
"settings|preferences|keyboard_view_shortcuts_button",
|
||||||
{},
|
{},
|
||||||
@@ -293,7 +296,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
{this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.KEYBINDINGS_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|time_heading")}>
|
<SettingsSubsection heading={_t("settings|preferences|time_heading")} formWrap>
|
||||||
<div className="mx_SettingsSubsection_dropdown">
|
<div className="mx_SettingsSubsection_dropdown">
|
||||||
{_t("settings|preferences|user_timezone")}
|
{_t("settings|preferences|user_timezone")}
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@@ -316,38 +319,39 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection
|
<SettingsSubsection
|
||||||
|
formWrap
|
||||||
heading={_t("common|presence")}
|
heading={_t("common|presence")}
|
||||||
description={_t("settings|preferences|presence_description")}
|
description={_t("settings|preferences|presence_description")}
|
||||||
>
|
>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|composer_heading")}>
|
<SettingsSubsection formWrap heading={_t("settings|preferences|composer_heading")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.COMPOSER_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|code_blocks_heading")}>
|
<SettingsSubsection formWrap heading={_t("settings|preferences|code_blocks_heading")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.CODE_BLOCKS_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|media_heading")}>
|
<SettingsSubsection formWrap heading={_t("settings|preferences|media_heading")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.IMAGES_AND_VIDEOS_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("common|timeline")}>
|
<SettingsSubsection formWrap heading={_t("common|timeline")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.TIMELINE_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
<SettingsSubsection formWrap heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||||
<MediaPreviewAccountSettings />
|
<MediaPreviewAccountSettings />
|
||||||
<InviteRulesAccountSetting />
|
<InviteRulesAccountSetting />
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
|
<SettingsSubsection formWrap heading={_t("settings|preferences|room_directory_heading")}>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.ROOM_DIRECTORY_SETTINGS)}
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("common|general")} stretchContent>
|
<SettingsSubsection formWrap heading={_t("common|general")} stretchContent>
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS)}
|
||||||
|
|
||||||
<SettingsFlag name="Electron.showTrayIcon" level={SettingLevel.PLATFORM} hideIfCannotSet />
|
<SettingsFlag name="Electron.showTrayIcon" level={SettingLevel.PLATFORM} hideIfCannotSet />
|
||||||
|
|||||||