Compare commits
42 Commits
dbkr/fix_m
...
midhun/mod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
507eaa02df | ||
|
|
b94d40f166 | ||
|
|
c2d68f8dc0 | ||
|
|
3be766d79c | ||
|
|
335491eabc | ||
|
|
2449557aa8 | ||
|
|
eebf227cf4 | ||
|
|
ebc9e3ace6 | ||
|
|
61306a1e4a | ||
|
|
a9fed64637 | ||
|
|
8a875e8c6d | ||
|
|
620ba9231d | ||
|
|
f2104b5ec0 | ||
|
|
1c0738be0f | ||
|
|
c78461db0b | ||
|
|
2b05d51e41 | ||
|
|
6f6b3bdd8f | ||
|
|
da11cff6ff | ||
|
|
302b6567ea | ||
|
|
b8c79f46ee | ||
|
|
0e8a617beb | ||
|
|
a94328a125 | ||
|
|
4d7d06bfc0 | ||
|
|
c31d4fea8d | ||
|
|
a5f3876a38 | ||
|
|
206905c2f5 | ||
|
|
51499fa106 | ||
|
|
1ebead1c8a | ||
|
|
738eac9b90 | ||
|
|
2dd743dea0 | ||
|
|
ced886aa07 | ||
|
|
de5a75777f | ||
|
|
809b41aa59 | ||
|
|
b6b1658805 | ||
|
|
afa340eb18 | ||
|
|
7ac4a4a2d4 | ||
|
|
66bf1dd469 | ||
|
|
9ae447f14f | ||
|
|
a02a5ac849 | ||
|
|
e4dee7ab63 | ||
|
|
9129c35407 | ||
|
|
4b701b55b1 |
12
.github/workflows/shared-component-publish.yaml
vendored
@@ -21,14 +21,8 @@ jobs:
|
||||
cache: "yarn"
|
||||
node-version-file: ".node-version"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Ensure npm 11.5.1 or later is installed
|
||||
- name: Update npm
|
||||
run: npm install -g npm@latest
|
||||
|
||||
# Need to setup element web too as it needs the translations
|
||||
- name: 🛠️ Setup EW
|
||||
run: yarn install --pure-lockfile
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.ELEMENT_NPM_TOKEN }}
|
||||
|
||||
- name: 🛠️ Setup
|
||||
# When running `install` it also calls the `prepare` step which generates
|
||||
@@ -37,4 +31,4 @@ jobs:
|
||||
|
||||
- name: 🚀 Publish to npm
|
||||
working-directory: packages/shared-components
|
||||
run: npm publish --access public --tag test --provenance
|
||||
run: npm publish --access public --provenance
|
||||
|
||||
@@ -34,6 +34,10 @@ jobs:
|
||||
- name: Install element web dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build Element Web resources
|
||||
# Needed to prepare language files
|
||||
run: "yarn build:res"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: packages/shared-components
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
8
.github/workflows/static_analysis.yaml
vendored
@@ -35,6 +35,10 @@ jobs:
|
||||
- name: Typecheck
|
||||
run: "yarn run lint:types"
|
||||
|
||||
- name: Build Element Web resources
|
||||
# Needed to prepare language files for shared components
|
||||
run: "yarn build:res"
|
||||
|
||||
- name: Install Shared Component Dependencies
|
||||
run: "yarn --cwd packages/shared-components install"
|
||||
|
||||
@@ -87,6 +91,10 @@ jobs:
|
||||
- name: Run Linter
|
||||
run: "yarn run lint:js"
|
||||
|
||||
- name: Build Element Web resources
|
||||
# Needed to prepare language files for shared components
|
||||
run: "yarn build:res"
|
||||
|
||||
- name: Install Shared Component Deps
|
||||
run: "yarn --cwd packages/shared-components install --frozen-lockfile"
|
||||
|
||||
|
||||
61
.github/workflows/tests.yml
vendored
@@ -29,8 +29,8 @@ env:
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
jest_ew:
|
||||
name: Jest (Element Web)
|
||||
jest:
|
||||
name: Jest
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -93,13 +93,13 @@ jobs:
|
||||
|
||||
complete:
|
||||
name: jest-tests
|
||||
needs: jest_ew
|
||||
needs: jest
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
statuses: write
|
||||
steps:
|
||||
- if: needs.jest_ew.result != 'skipped' && needs.jest_ew.result != 'success'
|
||||
- if: needs.jest.result != 'skipped' && needs.jest.result != 'success'
|
||||
run: exit 1
|
||||
|
||||
- name: Skip SonarCloud in merge queue
|
||||
@@ -112,56 +112,3 @@ jobs:
|
||||
context: SonarCloud Code Analysis
|
||||
sha: ${{ github.sha }}
|
||||
target_url: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
jest_sc:
|
||||
name: Jest (Shared Components)
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
|
||||
with:
|
||||
repository: ${{ inputs.matrix-js-sdk-sha && 'element-hq/element-web' || github.repository }}
|
||||
|
||||
- name: Yarn cache
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
|
||||
with:
|
||||
node-version: "lts/*"
|
||||
cache: "yarn"
|
||||
|
||||
- name: Install EW Deps
|
||||
run: "yarn install"
|
||||
|
||||
- name: Install Shared Component Deps
|
||||
working-directory: "packages/shared-components"
|
||||
run: "yarn install"
|
||||
|
||||
- name: Jest Cache
|
||||
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
|
||||
with:
|
||||
path: /tmp/jest_cache
|
||||
key: ${{ hashFiles('**/yarn.lock') }}
|
||||
|
||||
- name: Get number of CPU cores
|
||||
id: cpu-cores
|
||||
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2
|
||||
|
||||
- name: Run tests
|
||||
working-directory: "packages/shared-components"
|
||||
run: |
|
||||
yarn test \
|
||||
--coverage=${{ env.ENABLE_COVERAGE }} \
|
||||
--ci \
|
||||
--max-workers ${{ steps.cpu-cores.outputs.count }} \
|
||||
--cacheDirectory /tmp/jest_cache
|
||||
env:
|
||||
# tell jest to use coloured output
|
||||
FORCE_COLOR: true
|
||||
|
||||
- name: Upload Artifact
|
||||
if: env.ENABLE_COVERAGE == 'true'
|
||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
|
||||
with:
|
||||
name: coverage-sharedcomponents
|
||||
path: |
|
||||
packages/shared-components/coverage
|
||||
!packages/shared-components/coverage/lcov-report
|
||||
|
||||
1
.gitignore
vendored
@@ -36,4 +36,3 @@ storybook-static
|
||||
|
||||
/packages/shared-components/node_modules
|
||||
/packages/shared-components/dist
|
||||
/packages/shared-components/src/i18nKeys.d.ts
|
||||
|
||||
24
CHANGELOG.md
@@ -1,27 +1,3 @@
|
||||
Changes in [1.12.3](https://github.com/element-hq/element-web/releases/tag/v1.12.3) (2025-11-04)
|
||||
================================================================================================
|
||||
## 🦖 Deprecations
|
||||
|
||||
* Remove allowVoipWithNoMedia feature flag ([#31087](https://github.com/element-hq/element-web/pull/31087)). Contributed by @Half-Shot.
|
||||
|
||||
## ✨ Features
|
||||
|
||||
* Change module API to be an instance getter ([#31025](https://github.com/element-hq/element-web/pull/31025)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* Show hover elements when keyboard focus is within an event tile ([#31078](https://github.com/element-hq/element-web/pull/31078)). Contributed by @t3chguy.
|
||||
* Ensure toolbar navigation pattern works in MessageActionBar ([#31080](https://github.com/element-hq/element-web/pull/31080)). Contributed by @t3chguy.
|
||||
* Ensure sent markers are hidden when showing thread summary. ([#31076](https://github.com/element-hq/element-web/pull/31076)). Contributed by @Half-Shot.
|
||||
* Fix translation in dev mode ([#31045](https://github.com/element-hq/element-web/pull/31045)). Contributed by @florianduros.
|
||||
* Fix sort order in space hierarchy ([#30975](https://github.com/element-hq/element-web/pull/30975)). Contributed by @t3chguy.
|
||||
* New Room list: don't display message preview of thread ([#31043](https://github.com/element-hq/element-web/pull/31043)). Contributed by @florianduros.
|
||||
* Revert "A11y: move focus to right panel when opened" ([#30999](https://github.com/element-hq/element-web/pull/30999)). Contributed by @florianduros.
|
||||
* Fix highlights in messages (or search results) breaking links ([#30264](https://github.com/element-hq/element-web/pull/30264)). Contributed by @bojidar-bg.
|
||||
* Add prepare script ([#31030](https://github.com/element-hq/element-web/pull/31030)). Contributed by @dbkr.
|
||||
* Fix html exports by adding SDKContext ([#30987](https://github.com/element-hq/element-web/pull/30987)). Contributed by @t3chguy.
|
||||
|
||||
|
||||
Changes in [1.12.2](https://github.com/element-hq/element-web/releases/tag/v1.12.2) (2025-10-21)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -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
|
||||
customExportConditions: ["browser", "node"],
|
||||
},
|
||||
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)"],
|
||||
testMatch: ["<rootDir>/test/**/*-test.[tj]s?(x)", "<rootDir>/packages/*/src/**/*.test.[t]s?(x)"],
|
||||
globalSetup: "<rootDir>/test/globalSetup.ts",
|
||||
setupFiles: ["jest-canvas-mock", "web-streams-polyfill/polyfill"],
|
||||
setupFilesAfterEnv: ["<rootDir>/test/setupTests.ts"],
|
||||
@@ -40,7 +40,6 @@ const config: Config = {
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
"counterpart": "<rootDir>/node_modules/counterpart",
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.3",
|
||||
"version": "1.12.2",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -72,7 +72,6 @@
|
||||
"@playwright/test": "1.56.1",
|
||||
"@types/react": "19.2.2",
|
||||
"@types/react-dom": "19.2.2",
|
||||
"@types/serve-static": "1.15.10",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001751",
|
||||
@@ -82,8 +81,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "1.4.1",
|
||||
"@element-hq/web-shared-components": "file:packages/shared-components",
|
||||
"@element-hq/element-web-module-api": "1.5.0",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
@@ -105,6 +103,7 @@
|
||||
"browserslist": "^4.23.2",
|
||||
"classnames": "^2.2.6",
|
||||
"commonmark": "^0.31.0",
|
||||
"counterpart": "^0.18.6",
|
||||
"css-tree": "^3.0.0",
|
||||
"diff-dom": "^5.0.0",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
plugins: ["matrix-org", "eslint-plugin-react-compiler"],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
dist/
|
||||
i18n/i18nKeys.d.ts
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
module.exports = {
|
||||
sourceMaps: true,
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
include: ["@babel/plugin-transform-class-properties"],
|
||||
},
|
||||
],
|
||||
["@babel/preset-typescript", { allowDeclareFields: true }],
|
||||
"@babel/preset-react",
|
||||
],
|
||||
plugins: [
|
||||
"@babel/plugin-proposal-export-default-from",
|
||||
"@babel/plugin-transform-numeric-separator",
|
||||
"@babel/plugin-transform-object-rest-spread",
|
||||
"@babel/plugin-transform-optional-chaining",
|
||||
"@babel/plugin-transform-nullish-coalescing-operator",
|
||||
"@babel/plugin-transform-runtime",
|
||||
],
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations 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 { env } from "process";
|
||||
|
||||
import type { Config } from "jest";
|
||||
|
||||
const config: Config = {
|
||||
testEnvironment: "jsdom",
|
||||
testEnvironmentOptions: {
|
||||
url: "http://localhost/",
|
||||
},
|
||||
testMatch: ["<rootDir>/src/**/*.test.[tj]s?(x)"],
|
||||
setupFilesAfterEnv: ["<rootDir>/src/test/setupTests.ts"],
|
||||
moduleNameMapper: {
|
||||
// Support CSS module
|
||||
"\\.(module.css)$": "identity-obj-proxy",
|
||||
"\\.(css|scss|pcss)$": "<rootDir>/__mocks__/cssMock.js",
|
||||
"\\.(gif|png|ttf|woff2)$": "<rootDir>/__mocks__/imageMock.js",
|
||||
"\\.svg$": "<rootDir>/__mocks__/svg.js",
|
||||
"\\$webapp/i18n/languages.json": "<rootDir>/../../__mocks__/languages.json",
|
||||
"^react$": "<rootDir>/node_modules/react",
|
||||
"^react-dom$": "<rootDir>/node_modules/react-dom",
|
||||
"waveWorker\\.min\\.js": "<rootDir>/__mocks__/empty.js",
|
||||
"context-filter-polyfill": "<rootDir>/__mocks__/empty.js",
|
||||
"workers/(.+)Factory": "<rootDir>/__mocks__/workerFactoryMock.js",
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
"/node_modules/(?!(mime|matrix-js-sdk|uuid|p-retry|is-network-error|react-merge-refs)).+$",
|
||||
],
|
||||
collectCoverageFrom: [
|
||||
"<rootDir>/src/**/*.{js,ts,tsx}",
|
||||
"<rootDir>/packages/**/*.{js,ts,tsx}",
|
||||
// Coverage chokes on type definition files
|
||||
"!<rootDir>/src/**/*.d.ts",
|
||||
],
|
||||
coverageReporters: ["text-summary", "lcov"],
|
||||
testResultsProcessor: "@casualbot/jest-sonar-reporter",
|
||||
prettierPath: null,
|
||||
moduleDirectories: ["node_modules", "./src/test/utils"],
|
||||
};
|
||||
|
||||
// if we're running under GHA, enable the GHA reporter
|
||||
if (env["GITHUB_ACTIONS"] !== undefined) {
|
||||
const reporters: Config["reporters"] = [["github-actions", { silent: false }], "summary"];
|
||||
|
||||
// if we're running against the develop branch, also enable the slow test reporter
|
||||
if (env["GITHUB_REF"] == "refs/heads/develop") {
|
||||
reporters.push("<rootDir>/test/slowReporter.cjs");
|
||||
}
|
||||
config.reporters = reporters;
|
||||
}
|
||||
|
||||
export default config;
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@element-hq/web-shared-components",
|
||||
"version": "0.0.0-test.7",
|
||||
"version": "0.0.0-test.6",
|
||||
"description": "Shared components for Element",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -19,10 +19,6 @@
|
||||
"types": "./dist/element-web-shared-components.d.ts",
|
||||
"default": "./dist/element-web-shared-components.mjs"
|
||||
}
|
||||
},
|
||||
"./dist/element-web-shared-components.css": {
|
||||
"require": "./dist/element-web-shared-components.css",
|
||||
"import": "./dist/element-web-shared-components.css"
|
||||
}
|
||||
},
|
||||
"types": "dist/element-web-shared-components.d.ts",
|
||||
@@ -34,8 +30,8 @@
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"prepare": "patch-package && yarn --cwd ../.. build:res && ts-node scripts/gatherTranslationKeys.ts && vite build",
|
||||
"postinstall": "patch-package",
|
||||
"prepare": "vite build",
|
||||
"storybook": "storybook dev -p 6007",
|
||||
"build-storybook": "storybook build",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
@@ -46,13 +42,9 @@
|
||||
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
||||
},
|
||||
"dependencies": {
|
||||
"classnames": "^2.5.1",
|
||||
"counterpart": "^0.18.6",
|
||||
"lodash": "^4.17.21",
|
||||
"matrix-web-i18n": "^3.4.0",
|
||||
"patch-package": "^8.0.1",
|
||||
"react-merge-refs": "^3.0.2",
|
||||
"temporal-polyfill": "^0.3.0"
|
||||
"counterpart": "^0.18.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-a11y": "^9.1.10",
|
||||
@@ -61,21 +53,13 @@
|
||||
"@storybook/icons": "^1.6.0",
|
||||
"@storybook/react-vite": "^9.1.10",
|
||||
"@storybook/test-runner": "^0.23.0",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@types/counterpart": "^0.18.4",
|
||||
"@types/lodash": "^4.17.20",
|
||||
"@types/react": "^19.2.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"eslint": "8",
|
||||
"eslint-plugin-matrix-org": "^3.0.0",
|
||||
"eslint-plugin-storybook": "^10.0.0",
|
||||
"jest": "^30.2.0",
|
||||
"jest-image-snapshot": "^6.5.1",
|
||||
"patch-package": "^8.0.1",
|
||||
"prettier": "^3.6.2",
|
||||
"storybook": "^9.1.10",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.3",
|
||||
"vite": "^7.1.9",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
@@ -84,9 +68,5 @@
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"peerDependencies": {
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.2.5"
|
||||
}
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations 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.
|
||||
*/
|
||||
|
||||
// Gathers all the translation keys from element-web's en_EN.json into a TypeScript type definition file
|
||||
// that exports a type `TranslationKey` which is a union of all supported translation keys.
|
||||
// This prevents having to import the json file and make typescript do the work as this results in vite-dts
|
||||
// generating an import to the json file in the .d.ts which doesn't work at runtime: this way, the type
|
||||
// gets put into the bundle.
|
||||
// XXX: It should *not* be in the 'src' directory, being a generated file, but if it isn't then the type
|
||||
// bundler won't bundle the types and will leave the file as a relative import, which will break.
|
||||
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
const i18nStringsPath = path.resolve(__dirname, "../../../src/i18n/strings/en_EN.json");
|
||||
const outPath = path.resolve(__dirname, "../src/i18nKeys.d.ts");
|
||||
|
||||
function gatherKeys(obj: any, prefix: string[] = []): string[] {
|
||||
if (typeof obj !== "object" || obj === null) return [];
|
||||
let keys: string[] = [];
|
||||
for (const key of Object.keys(obj)) {
|
||||
const value = obj[key];
|
||||
|
||||
// add the path (for both leaves and intermediates as then we include plurals)
|
||||
keys.push([...prefix, key].join("|"));
|
||||
if (typeof value === "object" && value !== null) {
|
||||
// If the value is an object, recurse
|
||||
keys = keys.concat(gatherKeys(value, [...prefix, key]));
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const json = JSON.parse(fs.readFileSync(i18nStringsPath, "utf8"));
|
||||
const keys = gatherKeys(json);
|
||||
const typeDef =
|
||||
"/*\n" +
|
||||
" * Copyright 2025 Element Creations Ltd.\n" +
|
||||
" *\n" +
|
||||
" * SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial\n" +
|
||||
" * Please see LICENSE files in the repository root for full details.\n" +
|
||||
" */\n" +
|
||||
"\n" +
|
||||
"// This file is auto-generated by gatherTranslationKeys.ts\n" +
|
||||
"// Do not edit manually.\n\n" +
|
||||
"export type TranslationKey =\n" +
|
||||
keys.map((k) => ` | \"${k}\"`).join("\n") +
|
||||
";\n";
|
||||
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
||||
fs.writeFileSync(outPath, typeDef, "utf8");
|
||||
console.log(`Wrote ${keys.length} keys to ${outPath}`);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
.audioPlayer {
|
||||
padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x) !important;
|
||||
padding: var(--cpd-space-4x) var(--cpd-space-3x) var(--cpd-space-3x) var(--cpd-space-3x);
|
||||
}
|
||||
|
||||
.mediaInfo {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AudioPlayerView renders the audio player in default state 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Clock renders the clock 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
*/
|
||||
|
||||
.button {
|
||||
border-radius: 32px !important;
|
||||
background-color: var(--cpd-color-bg-subtle-primary) !important;
|
||||
border-radius: 32px;
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PlayPauseButton renders the button in default state 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Seekbar renders the clock 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`AvatarWithDetails renders a textual event 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TextualEventView renders a textual event 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -5,4 +5,4 @@ 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.
|
||||
*/
|
||||
|
||||
export { TextualEventView, type TextualEventViewSnapshot } from "./TextualEventView";
|
||||
export { TextualEventView } from "./TextualEventView";
|
||||
|
||||
@@ -21,17 +21,12 @@ export * from "./utils/Box";
|
||||
export * from "./utils/Flex";
|
||||
|
||||
// Utils
|
||||
export * from "./utils/i18n";
|
||||
export { setLanguage } from "./utils/i18n";
|
||||
export * from "./utils/humanize";
|
||||
export * from "./utils/DateUtils";
|
||||
export * from "./utils/numbers";
|
||||
export * from "./utils/FormattingUtils";
|
||||
|
||||
// MVVM
|
||||
export * from "./viewmodel";
|
||||
export * from "./useMockedViewModel";
|
||||
export * from "./useViewModel";
|
||||
|
||||
// i18n (we must export this directly in order to not confuse the type bundler, it seems,
|
||||
// otherwise it will leave it as a relative import rather than bundling it)
|
||||
export type * from "./i18nKeys.d.ts";
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
.mediaBody {
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
border-radius: var(--cpd-space-2x) !important;
|
||||
border-radius: var(--cpd-space-2x);
|
||||
max-width: 243px; /* use max-width instead of width so it fits within right panels */
|
||||
|
||||
font: var(--cpd-font-body-md-regular);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`MediaBody renders the media body 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Pill renders the pill 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PillInput renders only the input without children 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RichItem renders the item in default state 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RichItem renders the list 1`] = `
|
||||
<div>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations 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 fetchMock from "fetch-mock-jest";
|
||||
|
||||
import { setLanguage } from "../../src/utils/i18n";
|
||||
import en from "../../../../src/i18n/strings/en_EN.json";
|
||||
|
||||
export function setupLanguageMock(): void {
|
||||
fetchMock
|
||||
.get("/i18n/languages.json", {
|
||||
en: "en_EN.json",
|
||||
})
|
||||
.get("end:en_EN.json", en);
|
||||
}
|
||||
setupLanguageMock();
|
||||
|
||||
setLanguage("en");
|
||||
@@ -1,47 +0,0 @@
|
||||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// Copied from element-web/test/test-utils because, seemingly, if we
|
||||
// set that as the modules directory to use it directly, it fails to
|
||||
// actually put the right thing in the context somehow.
|
||||
|
||||
import React, { type ReactElement } from "react";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { render, type RenderOptions } from "@testing-library/react";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
|
||||
return ({ children }: { children: React.ReactNode }) => {
|
||||
if (Wrapper) {
|
||||
return (
|
||||
<Wrapper>
|
||||
<TooltipProvider>{children}</TooltipProvider>
|
||||
</Wrapper>
|
||||
);
|
||||
} else {
|
||||
return <TooltipProvider>{children}</TooltipProvider>;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const customRender = (ui: ReactElement, options: RenderOptions = {}): ReturnType<typeof render> => {
|
||||
return render(ui, {
|
||||
...options,
|
||||
wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"],
|
||||
}) as ReturnType<typeof render>;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
export * from "@testing-library/react";
|
||||
|
||||
/**
|
||||
* This custom render function wraps your component with a TooltipProvider.
|
||||
* See https://testing-library.com/docs/react-testing-library/setup/#custom-render
|
||||
*/
|
||||
export { customRender as render };
|
||||
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* Copyright 2025 Element Creations 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 counterpart from "counterpart";
|
||||
|
||||
import { registerTranslations, setMissingEntryGenerator, getLocale, setLocale } from "./i18n";
|
||||
|
||||
describe("i18n utils", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should wrap registerTranslations", () => {
|
||||
jest.spyOn(counterpart, "registerTranslations");
|
||||
|
||||
registerTranslations("en", { test: "This is a test" });
|
||||
expect(counterpart.registerTranslations).toHaveBeenCalledWith("en", { test: "This is a test" });
|
||||
});
|
||||
|
||||
it("should wrap setMissingEntryGenerator", () => {
|
||||
jest.spyOn(counterpart, "setMissingEntryGenerator");
|
||||
|
||||
const dummyFn = jest.fn();
|
||||
|
||||
setMissingEntryGenerator(dummyFn);
|
||||
expect(counterpart.setMissingEntryGenerator).toHaveBeenCalledWith(dummyFn);
|
||||
});
|
||||
|
||||
it("should wrap getLocale", () => {
|
||||
jest.spyOn(counterpart, "getLocale");
|
||||
|
||||
getLocale();
|
||||
expect(counterpart.getLocale).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should wrap setLocale", () => {
|
||||
jest.spyOn(counterpart, "setLocale");
|
||||
|
||||
setLocale("en");
|
||||
expect(counterpart.setLocale).toHaveBeenCalledWith("en");
|
||||
});
|
||||
});
|
||||
@@ -22,10 +22,10 @@
|
||||
* @return a React <span> component if any non-strings were used in substitutions, otherwise a string
|
||||
*/
|
||||
import React from "react";
|
||||
import { KEY_SEPARATOR } from "matrix-web-i18n";
|
||||
import { type TranslationKey as _TranslationKey, KEY_SEPARATOR } from "matrix-web-i18n";
|
||||
import counterpart from "counterpart";
|
||||
|
||||
import type { TranslationKey } from "../index";
|
||||
import type Translations from "../../../../src/i18n/strings/en_EN.json";
|
||||
|
||||
// @ts-ignore - $webapp is a webpack resolve alias pointing to the output directory, see webpack config
|
||||
import webpackLangJsonUrl from "$webapp/i18n/languages.json";
|
||||
@@ -45,23 +45,16 @@ counterpart.setSeparator(KEY_SEPARATOR);
|
||||
const FALLBACK_LOCALE = "en";
|
||||
counterpart.setFallbackLocale(FALLBACK_LOCALE);
|
||||
|
||||
// export wrappers around these functions because if we used counterpart directly from
|
||||
// element-web, it operates on a different instance of counterpart
|
||||
export function registerTranslations(locale: string, data: object): void {
|
||||
counterpart.registerTranslations(locale, data);
|
||||
}
|
||||
|
||||
export function setMissingEntryGenerator(callback: (value: string) => void): void {
|
||||
counterpart.setMissingEntryGenerator(callback);
|
||||
}
|
||||
|
||||
export function getLocale(): string {
|
||||
return counterpart.getLocale();
|
||||
}
|
||||
|
||||
export function setLocale(value: string): string {
|
||||
return counterpart.setLocale(value);
|
||||
}
|
||||
/**
|
||||
* A type representing the union of possible keys into the translation file using `|` delimiter to access nested fields.
|
||||
* @example `common|error` to access `error` within the `common` sub-object.
|
||||
* {
|
||||
* "common": {
|
||||
* "error": "Error"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
export type TranslationKey = _TranslationKey<typeof Translations>;
|
||||
|
||||
// Function which only purpose is to mark that a string is translatable
|
||||
// Does not actually do anything. It's helpful for automatic extraction of translatable strings
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"lib": ["es2022", "es2024.promise", "dom", "dom.iterable"],
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"jest-matrix-react": ["./src/test/utils/jest-matrix-react"],
|
||||
"jest-matrix-react": ["../../test/test-utils/jest-matrix-react"],
|
||||
"rollup/parseAst": ["./node_modules/rollup/dist/parseAst"]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ export default defineConfig({
|
||||
rollupOptions: {
|
||||
// make sure to externalize deps that shouldn't be bundled
|
||||
// into your library
|
||||
external: ["react", "react-dom", "@vector-im/compound-design-tokens", "@vector-im/compound-web"],
|
||||
external: ["react", "react-dom"],
|
||||
output: {
|
||||
// Provide global variables to use in the UMD build
|
||||
// for externalized deps
|
||||
@@ -43,12 +43,5 @@ export default defineConfig({
|
||||
$webapp: resolve(__dirname, "..", "..", "webapp"),
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
dts({
|
||||
rollupTypes: true,
|
||||
include: ["src/**/*.{ts,tsx}"],
|
||||
exclude: ["src/**/*.test.{ts,tsx}"],
|
||||
copyDtsFiles: true,
|
||||
}),
|
||||
],
|
||||
plugins: [dts({ rollupTypes: true, include: ["src/**/*.{ts,tsx}"], copyDtsFiles: true })],
|
||||
});
|
||||
|
||||
@@ -76,57 +76,6 @@ test.describe("Composer", () => {
|
||||
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
||||
});
|
||||
|
||||
test.describe("render emoji picker with larger viewport height", async () => {
|
||||
test.use({ viewport: { width: 1280, height: 720 } });
|
||||
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
|
||||
await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker.png");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("render emoji picker with small viewport height", async () => {
|
||||
test.use({ viewport: { width: 1280, height: 360 } });
|
||||
test("render emoji picker", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
await app.getComposer(false).getByRole("button", { name: "Emoji" }).click();
|
||||
await expect(page.getByTestId("mx_EmojiPicker")).toMatchScreenshot("emoji-picker-small.png");
|
||||
});
|
||||
});
|
||||
|
||||
test("should have focus lock in emoji picker", async ({ page, app }) => {
|
||||
const emojiButton = app.getComposer(false).getByRole("button", { name: "Emoji" });
|
||||
|
||||
// Open emoji picker by clicking the button
|
||||
await emojiButton.click();
|
||||
|
||||
// Wait for emoji picker to be visible
|
||||
const emojiPicker = page.getByTestId("mx_EmojiPicker");
|
||||
await expect(emojiPicker).toBeVisible();
|
||||
|
||||
// Get initial focused element (should be search input)
|
||||
const searchInput = emojiPicker.getByRole("textbox", { name: "Search" });
|
||||
await expect(searchInput).toBeFocused();
|
||||
|
||||
// Try to tab multiple times - focus should stay within emoji picker
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Tab");
|
||||
await page.keyboard.press("Tab");
|
||||
|
||||
// Verify we're still within the emoji picker (not back to composer)
|
||||
const focusedElement = await page.evaluate(() => document.activeElement?.closest(".mx_EmojiPicker"));
|
||||
expect(focusedElement).not.toBeNull();
|
||||
|
||||
// Close with Escape key
|
||||
await page.keyboard.press("Escape");
|
||||
|
||||
// Verify emoji picker is closed
|
||||
await expect(emojiPicker).not.toBeVisible();
|
||||
|
||||
// Verify focus returns to emoji button
|
||||
await expect(emojiButton).toBeFocused();
|
||||
});
|
||||
|
||||
test.describe("when Control+Enter is required to send", () => {
|
||||
test.beforeEach(async ({ app }) => {
|
||||
await app.settings.setValue("MessageComposerInput.ctrlEnterToSend", null, SettingLevel.ACCOUNT, true);
|
||||
|
||||
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 9.3 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 33 KiB |
@@ -10,7 +10,7 @@ import {
|
||||
type StartedPostgreSqlContainer,
|
||||
} from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "main@sha256:0809c1870789e6c24f1344006b60d8bc9f8ca26f4ae4ba0d7957a1caf77e2be3";
|
||||
const TAG = "main@sha256:3d139198268dd06c42fe0fc7ffea5df0f2924704867f68e0a8eb3e542f91b9e8";
|
||||
|
||||
/**
|
||||
* MatrixAuthenticationServiceContainer which freezes the docker digest to
|
||||
|
||||
@@ -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";
|
||||
|
||||
const TAG = "develop@sha256:450b176d61e75b73d1acbbfab1de5ec3ebe08ba0f08ec00da872f7b5aaa23e54";
|
||||
const TAG = "develop@sha256:5a505af08294414dd832517ee0eafadc1faa33a675e577ca073c6fa731c7e5a8";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -211,6 +211,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_withIcon .mx_SpaceButton_icon {
|
||||
background-color: $panel-actions;
|
||||
}
|
||||
|
||||
&.mx_SpaceButton_home .mx_SpaceButton_icon::before {
|
||||
mask-image: url("@vector-im/compound-design-tokens/icons/home-solid.svg");
|
||||
}
|
||||
|
||||
@@ -8,8 +8,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_EmojiPicker {
|
||||
width: 340px;
|
||||
height: min(450px, 80vh);
|
||||
max-height: 80vh;
|
||||
height: 450px;
|
||||
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -182,7 +181,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EmojiPicker_body_showHighlight .mx_EmojiPicker_item_wrapper [tabindex="0"] .mx_EmojiPicker_item {
|
||||
.mx_EmojiPicker_body .mx_EmojiPicker_item_wrapper[tabindex="0"] .mx_EmojiPicker_item {
|
||||
background-color: $focus-bg-color;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,18 +8,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_MPollBody {
|
||||
margin-top: 8px;
|
||||
min-width: 0; /* Override fieldset default min-width: min-content */
|
||||
width: 100%; /* Ensure fieldset takes full available width */
|
||||
border: none; /* Remove default fieldset border */
|
||||
padding: 0; /* Remove default fieldset padding */
|
||||
|
||||
legend {
|
||||
h2 {
|
||||
font-weight: var(--cpd-font-weight-semibold);
|
||||
font-size: $font-15px;
|
||||
line-height: $font-24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: var(--cpd-font-letter-spacing-heading-lg);
|
||||
|
||||
.mx_MPollBody_edited {
|
||||
color: $roomtopic-color;
|
||||
@@ -28,7 +23,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
}
|
||||
|
||||
legend::before {
|
||||
h2::before {
|
||||
content: "";
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
|
||||
@@ -153,7 +153,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_MediaBody {
|
||||
/* leave space for the timestamp */
|
||||
padding-right: 48px !important;
|
||||
padding-right: 48px;
|
||||
}
|
||||
|
||||
.mx_MImageBody {
|
||||
|
||||
@@ -17,10 +17,4 @@ else
|
||||
fi
|
||||
|
||||
DIST_VERSION=$("$DIR"/normalize-version.sh "$DIST_VERSION")
|
||||
|
||||
yarn --cwd packages/shared-components install
|
||||
yarn --cwd packages/shared-components link
|
||||
|
||||
yarn link @element-hq/web-shared-components
|
||||
|
||||
VERSION=$DIST_VERSION yarn build
|
||||
|
||||
@@ -44,11 +44,3 @@ fi
|
||||
yarn link matrix-js-sdk
|
||||
[ -d matrix-analytics-events ] && yarn link @matrix-org/analytics-events
|
||||
yarn install --frozen-lockfile $@
|
||||
|
||||
# Link shared components
|
||||
pushd packages/shared-components
|
||||
yarn link
|
||||
yarn install --frozen-lockfile
|
||||
popd
|
||||
|
||||
yarn link @element-hq/web-shared-components
|
||||
|
||||
@@ -6,12 +6,12 @@ sonar.organization=element-hq
|
||||
|
||||
sonar.sources=src,res,packages/shared-components/src
|
||||
sonar.tests=test,playwright,src,packages
|
||||
sonar.test.inclusions=test/*,playwright/*,src/**/*.test.*,packages/*/src/**/*.test.*,packages/*/src/test/**/*
|
||||
sonar.test.inclusions=test/*,playwright/*,src/**/*.test.*,packages/*/src/**/*.test.*
|
||||
sonar.exclusions=__mocks__,docs,element.io,nginx
|
||||
|
||||
sonar.cpd.exclusions=src/i18n/strings/*.json
|
||||
sonar.typescript.tsconfigPath=./tsconfig.json
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info,packages/shared-components/coverage/lcov.info
|
||||
sonar.javascript.lcov.reportPaths=coverage/lcov.info
|
||||
sonar.coverage.exclusions=\
|
||||
test/**/*,\
|
||||
playwright/**/*,\
|
||||
@@ -21,6 +21,5 @@ sonar.coverage.exclusions=\
|
||||
src/components/views/dialogs/devtools/**/*,\
|
||||
src/utils/SessionLock.ts,\
|
||||
src/**/*.d.ts,\
|
||||
src/vector/mobile_guide/**/* \
|
||||
packages/shared-components/src/test/**/*
|
||||
src/vector/mobile_guide/**/*
|
||||
sonar.testExecutionReportPaths=coverage/jest-sonar-report.xml
|
||||
|
||||
@@ -13,7 +13,7 @@ import { type Optional } from "matrix-events-sdk";
|
||||
import { _t, getUserLanguage } from "./languageHandler";
|
||||
import { getUserTimezone } from "./TimezoneHandler";
|
||||
|
||||
export { formatSeconds } from "@element-hq/web-shared-components";
|
||||
export { formatSeconds } from "../packages/shared-components/src/utils/DateUtils";
|
||||
|
||||
export const MINUTE_MS = 60000;
|
||||
export const HOUR_MS = MINUTE_MS * 60;
|
||||
|
||||
@@ -31,7 +31,7 @@ const notLoggedInMap: Record<Exclude<Views, Views.LOGGED_IN>, ScreenName> = {
|
||||
[Views.LOCK_STOLEN]: "SessionLockStolen",
|
||||
};
|
||||
|
||||
const loggedInPageTypeMap: Record<PageType, ScreenName> = {
|
||||
const loggedInPageTypeMap: Record<PageType | string, ScreenName> = {
|
||||
[PageType.HomePage]: "Home",
|
||||
[PageType.RoomView]: "Room",
|
||||
[PageType.UserView]: "User",
|
||||
@@ -48,10 +48,10 @@ export default class PosthogTrackers {
|
||||
}
|
||||
|
||||
private view: Views = Views.LOADING;
|
||||
private pageType?: PageType;
|
||||
private pageType?: PageType | string;
|
||||
private override?: ScreenName;
|
||||
|
||||
public trackPageChange(view: Views, pageType: PageType | undefined, durationMs: number): void {
|
||||
public trackPageChange(view: Views, pageType: PageType | string | undefined, durationMs: number): void {
|
||||
this.view = view;
|
||||
this.pageType = pageType;
|
||||
if (this.override) return;
|
||||
|
||||
@@ -9,8 +9,7 @@ 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 "@element-hq/web-shared-components";
|
||||
|
||||
import { _td, type TranslationKey } from "../../packages/shared-components/src/utils/i18n";
|
||||
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
|
||||
import { type IBaseSetting } from "../settings/Settings";
|
||||
import { type KeyCombo } from "../KeyBindingsManager";
|
||||
|
||||
@@ -8,9 +8,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
import React, { type JSX, type ReactNode } from "react";
|
||||
import { Text, Heading, Button, Separator } from "@vector-im/compound-web";
|
||||
import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out";
|
||||
import { Flex } from "@element-hq/web-shared-components";
|
||||
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import { Flex } from "../../../packages/shared-components/src/utils/Flex";
|
||||
import { _t } from "../../languageHandler";
|
||||
import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg";
|
||||
import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg";
|
||||
|
||||
@@ -9,13 +9,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
import EventEmitter from "events";
|
||||
import { SimpleObservable } from "matrix-widget-api";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { clamp } from "@element-hq/web-shared-components";
|
||||
|
||||
import { UPDATE_EVENT } from "../stores/AsyncStore";
|
||||
import { arrayFastResample } from "../utils/arrays";
|
||||
import { type IDestroyable } from "../utils/IDestroyable";
|
||||
import { PlaybackClock } from "./PlaybackClock";
|
||||
import { createAudioContext, decodeOgg } from "./compat";
|
||||
import { clamp } from "../../packages/shared-components/src/utils/numbers";
|
||||
import { DEFAULT_WAVEFORM, PLAYBACK_WAVEFORM_SAMPLES } from "./consts";
|
||||
import { PlaybackEncoder } from "../PlaybackEncoder";
|
||||
|
||||
@@ -202,7 +202,6 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
||||
private onPlaybackEnd = async (): Promise<void> => {
|
||||
await this.context.suspend();
|
||||
this.emit(PlaybackState.Stopped);
|
||||
this.clock.flagStop();
|
||||
};
|
||||
|
||||
public async play(): Promise<void> {
|
||||
@@ -249,8 +248,9 @@ export class Playback extends EventEmitter implements IDestroyable, PlaybackInte
|
||||
this.emit(PlaybackState.Paused);
|
||||
}
|
||||
|
||||
public stop(): Promise<void> {
|
||||
return this.onPlaybackEnd();
|
||||
public async stop(): Promise<void> {
|
||||
await this.onPlaybackEnd();
|
||||
this.clock.flagStop();
|
||||
}
|
||||
|
||||
public async toggle(): Promise<void> {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyrimport { type IAmplitudePayload, type ITimingPayload, PayloadEvent, WORKLET_NAME } from "./consts";
|
||||
import { percentageOf } from "../../packages/shared-components/src/utils/numbers";
|
||||
|
||||
// from AudioWorkletGlobalScope: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletGlobalScope" 2024 New Vector Ltd.
|
||||
Copyright 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 { percentageOf } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type IAmplitudePayload, type ITimingPayload, PayloadEvent, WORKLET_NAME } from "./consts";
|
||||
import { percentageOf } from "../../packages/shared-components/src/utils/numbers";
|
||||
|
||||
// from AudioWorkletGlobalScope: https://developer.mozilla.org/en-US/docs/Web/API/AudioWorkletGlobalScope
|
||||
declare const currentTime: number;
|
||||
|
||||
@@ -11,7 +11,6 @@ import encoderPath from "opus-recorder/dist/encoderWorker.min.js";
|
||||
import { SimpleObservable } from "matrix-widget-api";
|
||||
import EventEmitter from "events";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { clamp } from "@element-hq/web-shared-components";
|
||||
|
||||
import MediaDeviceHandler from "../MediaDeviceHandler";
|
||||
import { type IDestroyable } from "../utils/IDestroyable";
|
||||
@@ -20,6 +19,7 @@ import { PayloadEvent, WORKLET_NAME } from "./consts";
|
||||
import { UPDATE_EVENT } from "../stores/AsyncStore";
|
||||
import { createAudioContext } from "./compat";
|
||||
import { FixedRollingArray } from "../utils/FixedRollingArray";
|
||||
import { clamp } from "../../packages/shared-components/src/utils/numbers";
|
||||
import recorderWorkletFactory from "./recorderWorkletFactory";
|
||||
|
||||
const CHANNELS = 1; // stereo isn't important
|
||||
|
||||
@@ -12,7 +12,6 @@ import React, { type JSX, type CSSProperties, type RefObject, type SyntheticEven
|
||||
import ReactDOM from "react-dom";
|
||||
import classNames from "classnames";
|
||||
import FocusLock from "react-focus-lock";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
import { type Writeable } from "../../@types/common";
|
||||
import UIStore from "../../stores/UIStore";
|
||||
@@ -405,7 +404,7 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
|
||||
);
|
||||
|
||||
if (focusLock) {
|
||||
body = <FocusLock returnFocus>{body}</FocusLock>;
|
||||
body = <FocusLock>{body}</FocusLock>;
|
||||
}
|
||||
|
||||
// filter props that are invalid for DOM elements
|
||||
@@ -426,17 +425,15 @@ export default class ContextMenu extends React.PureComponent<React.PropsWithChil
|
||||
onContextMenu={this.onContextMenuPreventBubbling}
|
||||
>
|
||||
{background}
|
||||
<TooltipProvider>
|
||||
<div
|
||||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContextMenuRect}
|
||||
role={managed ? "menu" : undefined}
|
||||
{...divProps}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
<div
|
||||
className={menuClasses}
|
||||
style={menuStyle}
|
||||
ref={this.collectContextMenuRect}
|
||||
role={managed ? "menu" : undefined}
|
||||
{...divProps}
|
||||
>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</RovingTabIndexProvider>
|
||||
|
||||
@@ -68,7 +68,8 @@ import { monitorSyncedPushRules } from "../../utils/pushRules/monitorSyncedPushR
|
||||
import { type ConfigOptions } from "../../SdkConfig";
|
||||
import { MatrixClientContextProvider } from "./MatrixClientContextProvider";
|
||||
import { Landmark, LandmarkNavigation } from "../../accessibility/LandmarkNavigation";
|
||||
import { SDKContext, SdkContextClass } from "../../contexts/SDKContext.ts";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
import { SDKContext } from "../../contexts/SDKContext.ts";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
// so each pinned message may trigger a request. Limit the number per room for sanity.
|
||||
@@ -679,6 +680,10 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
public render(): React.ReactNode {
|
||||
let pageElement;
|
||||
|
||||
const moduleRenderer = this.props.page_type
|
||||
? ModuleApi.instance.navigation.locationRenderers.get(this.props.page_type)
|
||||
: undefined;
|
||||
|
||||
switch (this.props.page_type) {
|
||||
case PageTypes.RoomView:
|
||||
pageElement = (
|
||||
@@ -690,7 +695,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
key={this.props.currentRoomId || "roomview"}
|
||||
justCreatedOpts={this.props.roomJustCreatedOpts}
|
||||
forceTimeline={this.props.forceTimeline}
|
||||
roomViewStore={SdkContextClass.instance.roomViewStore}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
@@ -706,6 +710,13 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
);
|
||||
}
|
||||
break;
|
||||
default: {
|
||||
if (moduleRenderer) {
|
||||
pageElement = moduleRenderer();
|
||||
} else {
|
||||
console.warn(`Couldn't render page type "${this.props.page_type}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const wrapperClasses = classNames({
|
||||
@@ -747,20 +758,22 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||
)}
|
||||
<SpacePanel />
|
||||
{!useNewRoomList && <BackdropPanel backgroundImage={this.state.backgroundImage} />}
|
||||
<div
|
||||
className="mx_LeftPanel_wrapper--user"
|
||||
ref={this._resizeContainer}
|
||||
data-collapsed={shouldUseMinimizedUI ? true : undefined}
|
||||
>
|
||||
<LeftPanel
|
||||
pageType={this.props.page_type as PageTypes}
|
||||
isMinimized={shouldUseMinimizedUI || false}
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
/>
|
||||
</div>
|
||||
{!moduleRenderer && (
|
||||
<div
|
||||
className="mx_LeftPanel_wrapper--user"
|
||||
ref={this._resizeContainer}
|
||||
data-collapsed={shouldUseMinimizedUI ? true : undefined}
|
||||
>
|
||||
<LeftPanel
|
||||
pageType={this.props.page_type as PageTypes}
|
||||
isMinimized={shouldUseMinimizedUI || false}
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />
|
||||
{!moduleRenderer && <ResizeHandle passRef={this.resizeHandler} id="lp-resizer" />}
|
||||
<div className="mx_RoomView_wrapper">{pageElement}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,6 +140,7 @@ import { ShareFormat, type SharePayload } from "../../dispatcher/payloads/ShareP
|
||||
import Markdown from "../../Markdown";
|
||||
import { sanitizeHtmlParams } from "../../Linkify";
|
||||
import { isOnlyAdmin } from "../../utils/membership";
|
||||
import { ModuleApi } from "../../modules/Api.ts";
|
||||
|
||||
// legacy export
|
||||
export { default as Views } from "../../Views";
|
||||
@@ -175,9 +176,11 @@ interface IProps {
|
||||
interface IState {
|
||||
// the master view we are showing.
|
||||
view: Views;
|
||||
// What the LoggedInView would be showing if visible
|
||||
// What the LoggedInView would be showing if visible.
|
||||
// A member of the enum for standard pages or a string for those provided by
|
||||
// a module.
|
||||
// eslint-disable-next-line camelcase
|
||||
page_type?: PageType;
|
||||
page_type?: PageType | string;
|
||||
// The ID of the room we're viewing. This is either populated directly
|
||||
// in the case where we view a room by ID or by RoomView when it resolves
|
||||
// what ID an alias points at.
|
||||
@@ -1922,7 +1925,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||
subAction: params?.action,
|
||||
});
|
||||
} else {
|
||||
logger.info(`Ignoring showScreen for '${screen}'`);
|
||||
if (ModuleApi.instance.navigation.locationRenderers.get(screen)) {
|
||||
this.setState({ page_type: screen });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ import { type CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { debounce, throttle } from "lodash";
|
||||
import { CryptoEvent } from "matrix-js-sdk/src/crypto-api";
|
||||
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { type RoomViewProps } from "@element-hq/element-web-module-api";
|
||||
|
||||
import shouldHideEvent from "../../shouldHideEvent";
|
||||
import { _t } from "../../languageHandler";
|
||||
@@ -148,7 +149,7 @@ if (DEBUG) {
|
||||
debuglog = logger.log.bind(console);
|
||||
}
|
||||
|
||||
interface IRoomProps {
|
||||
interface IRoomProps extends RoomViewProps {
|
||||
threepidInvite?: IThreepidInvite;
|
||||
oobData?: IOOBData;
|
||||
|
||||
@@ -158,19 +159,17 @@ interface IRoomProps {
|
||||
|
||||
// Called with the credentials of a registered user (if they were a ROU that transitioned to PWLU)
|
||||
onRegistered?(credentials: IMatrixClientCreds): void;
|
||||
|
||||
/**
|
||||
* The RoomViewStore instance for the room to be displayed.
|
||||
* Only necessary if RoomView should get it's RoomViewStore through the MultiRoomViewStore.
|
||||
* Omitting this will mean that RoomView renders for the room held in SDKContext.RoomViewStore.
|
||||
*/
|
||||
roomViewStore: RoomViewStore;
|
||||
roomId?: string;
|
||||
}
|
||||
|
||||
export { MainSplitContentType };
|
||||
|
||||
export interface IRoomState {
|
||||
/**
|
||||
* The RoomViewStore instance for the room we are displaying
|
||||
*/
|
||||
roomViewStore: RoomViewStore;
|
||||
room?: Room;
|
||||
roomId?: string;
|
||||
roomAlias?: string;
|
||||
@@ -389,6 +388,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
private messagePanel: TimelinePanel | null = null;
|
||||
private roomViewBody = createRef<HTMLDivElement>();
|
||||
|
||||
private roomViewStore: RoomViewStore;
|
||||
|
||||
public static contextType = SDKContext;
|
||||
declare public context: React.ContextType<typeof SDKContext>;
|
||||
|
||||
@@ -401,9 +402,14 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
throw new Error("Unable to create RoomView without MatrixClient");
|
||||
}
|
||||
|
||||
if (props.roomId) {
|
||||
this.roomViewStore = this.context.multiRoomViewStore.getRoomViewStoreForRoom(props.roomId);
|
||||
} else {
|
||||
this.roomViewStore = context.roomViewStore;
|
||||
}
|
||||
|
||||
const llMembers = context.client.hasLazyLoadMembersEnabled();
|
||||
this.state = {
|
||||
roomViewStore: props.roomViewStore,
|
||||
roomId: undefined,
|
||||
roomLoading: true,
|
||||
peekLoading: false,
|
||||
@@ -535,7 +541,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
};
|
||||
|
||||
private getMainSplitContentType = (room: Room): MainSplitContentType => {
|
||||
if (this.state.roomViewStore.isViewingCall() || isVideoRoom(room)) {
|
||||
if (this.roomViewStore.isViewingCall() || isVideoRoom(room)) {
|
||||
return MainSplitContentType.Call;
|
||||
}
|
||||
if (this.context.widgetLayoutStore.hasMaximisedWidget(room)) {
|
||||
@@ -549,8 +555,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
return;
|
||||
}
|
||||
|
||||
const roomLoadError = this.state.roomViewStore.getRoomLoadError() ?? undefined;
|
||||
if (!initial && !roomLoadError && this.state.roomId !== this.state.roomViewStore.getRoomId()) {
|
||||
const roomLoadError = this.roomViewStore.getRoomLoadError() ?? undefined;
|
||||
if (!initial && !roomLoadError && this.state.roomId !== this.roomViewStore.getRoomId()) {
|
||||
// RoomView explicitly does not support changing what room
|
||||
// is being viewed: instead it should just be re-mounted when
|
||||
// switching rooms. Therefore, if the room ID changes, we
|
||||
@@ -564,7 +570,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// it was, it means we're about to be unmounted.
|
||||
return;
|
||||
}
|
||||
const roomViewStore = this.state.roomViewStore;
|
||||
const roomViewStore = this.roomViewStore;
|
||||
const roomId = roomViewStore.getRoomId() ?? null;
|
||||
const roomAlias = roomViewStore.getRoomAlias() ?? undefined;
|
||||
const roomLoading = roomViewStore.isRoomLoading();
|
||||
@@ -611,7 +617,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
newState.showRightPanel = false;
|
||||
}
|
||||
|
||||
const initialEventId = this.state.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
|
||||
const initialEventId = this.roomViewStore.getInitialEventId() ?? this.state.initialEventId;
|
||||
if (initialEventId) {
|
||||
let initialEvent = room?.findEventById(initialEventId);
|
||||
// The event does not exist in the current sync data
|
||||
@@ -637,13 +643,13 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
action: Action.ShowThread,
|
||||
rootEvent: thread.rootEvent,
|
||||
initialEvent,
|
||||
highlighted: this.state.roomViewStore.isInitialEventHighlighted(),
|
||||
scroll_into_view: this.state.roomViewStore.initialEventScrollIntoView(),
|
||||
highlighted: this.roomViewStore.isInitialEventHighlighted(),
|
||||
scroll_into_view: this.roomViewStore.initialEventScrollIntoView(),
|
||||
});
|
||||
} else {
|
||||
newState.initialEventId = initialEventId;
|
||||
newState.isInitialEventHighlighted = this.state.roomViewStore.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = this.state.roomViewStore.initialEventScrollIntoView();
|
||||
newState.isInitialEventHighlighted = this.roomViewStore.isInitialEventHighlighted();
|
||||
newState.initialEventScrollIntoView = this.roomViewStore.initialEventScrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -903,7 +909,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
this.context.client.on(MatrixEventEvent.Decrypted, this.onEventDecrypted);
|
||||
}
|
||||
// Start listening for RoomViewStore updates
|
||||
this.state.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.roomViewStore.on(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
|
||||
@@ -1020,7 +1026,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
window.removeEventListener("beforeunload", this.onPageUnload);
|
||||
|
||||
this.state.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
this.roomViewStore.off(UPDATE_EVENT, this.onRoomViewStoreUpdate);
|
||||
|
||||
this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate);
|
||||
WidgetEchoStore.removeListener(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
@@ -1048,6 +1054,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
// clean up if this was a local room
|
||||
this.context.client?.store.removeRoom(this.state.room.roomId);
|
||||
}
|
||||
|
||||
if (this.props.roomId) this.context.multiRoomViewStore.removeRoomViewStore(this.props.roomId);
|
||||
}
|
||||
|
||||
private onRightPanelStoreUpdate = (): void => {
|
||||
@@ -2070,7 +2078,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
if (!this.state.room || !this.context?.client) return null;
|
||||
const names = this.state.room.getDefaultRoomName(this.context.client.getSafeUserId());
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<LocalRoomCreateLoader
|
||||
localRoom={localRoom}
|
||||
names={names}
|
||||
@@ -2082,7 +2090,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
private renderLocalRoomView(localRoom: LocalRoom): ReactNode {
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<LocalRoomView
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
localRoom={localRoom}
|
||||
@@ -2098,7 +2106,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
|
||||
private renderWaitingForThirdPartyRoomView(inviteEvent: MatrixEvent): ReactNode {
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<WaitingForThirdPartyRoomView
|
||||
resizeNotifier={this.context.resizeNotifier}
|
||||
roomView={this.roomView}
|
||||
@@ -2640,7 +2648,7 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
}
|
||||
|
||||
return (
|
||||
<ScopedRoomContextProvider {...this.state}>
|
||||
<ScopedRoomContextProvider {...this.state} roomViewStore={this.roomViewStore}>
|
||||
<div className={mainClasses} ref={this.roomView} onKeyDown={this.onReactKeyDown}>
|
||||
{showChatEffects && this.roomView.current && (
|
||||
<EffectsOverlay roomWidth={this.roomView.current.offsetWidth} />
|
||||
|
||||
@@ -329,8 +329,8 @@ const SpaceSetupFirstRooms: React.FC<{
|
||||
return createRoom(space.client, {
|
||||
createOpts: {
|
||||
preset: isPublic ? Preset.PublicChat : Preset.PrivateChat,
|
||||
name,
|
||||
},
|
||||
name,
|
||||
spinner: false,
|
||||
encryption: false,
|
||||
andView: false,
|
||||
@@ -423,7 +423,7 @@ const SpaceSetupPublicShare: React.FC<ISpaceSetupPublicShareProps> = ({
|
||||
<div className="mx_SpaceRoomView_publicShare">
|
||||
<h1>
|
||||
{_t("create_space|share_heading", {
|
||||
name: justCreatedOpts?.name || space.name,
|
||||
name: justCreatedOpts?.createOpts?.name || space.name,
|
||||
})}
|
||||
</h1>
|
||||
<div className="mx_SpaceRoomView_description">{_t("create_space|share_description")}</div>
|
||||
@@ -449,7 +449,7 @@ const SpaceSetupPrivateScope: React.FC<{
|
||||
<h1>{_t("create_space|private_personal_heading")}</h1>
|
||||
<div className="mx_SpaceRoomView_description">
|
||||
{_t("create_space|private_personal_description", {
|
||||
name: justCreatedOpts?.name || space.name,
|
||||
name: justCreatedOpts?.createOpts?.name || space.name,
|
||||
})}
|
||||
</div>
|
||||
|
||||
@@ -686,7 +686,7 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
|
||||
<SpaceSetupFirstRooms
|
||||
space={this.props.space}
|
||||
title={_t("create_space|setup_rooms_community_heading", {
|
||||
spaceName: this.props.justCreatedOpts?.name || this.props.space.name,
|
||||
spaceName: this.props.justCreatedOpts?.createOpts?.name || this.props.space.name,
|
||||
})}
|
||||
description={
|
||||
<>
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface IListViewProps<Item, Context>
|
||||
index: number,
|
||||
item: Item,
|
||||
context: ListContext<Context>,
|
||||
onFocus: (item: Item, e: React.FocusEvent) => void,
|
||||
onFocus: (e: React.FocusEvent) => void,
|
||||
) => JSX.Element;
|
||||
|
||||
/**
|
||||
@@ -230,26 +230,19 @@ export function ListView<Item, Context = any>(props: IListViewProps<Item, Contex
|
||||
virtuosoDomRef.current = element;
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Focus handler passed to each item component.
|
||||
* Don't declare inside getItemComponent to avoid re-creating on each render.
|
||||
*/
|
||||
const onFocusForGetItemComponent = useCallback(
|
||||
(item: Item, e: React.FocusEvent) => {
|
||||
// If one of the item components has been focused directly, set the focused and tabIndex state
|
||||
// and stop propagation so the ListViews onFocus doesn't also handle it.
|
||||
const key = getItemKey(item);
|
||||
setIsFocused(true);
|
||||
setTabIndexKey(key);
|
||||
e.stopPropagation();
|
||||
},
|
||||
[getItemKey],
|
||||
);
|
||||
|
||||
const getItemComponentInternal = useCallback(
|
||||
(index: number, item: Item, context: ListContext<Context>): JSX.Element =>
|
||||
getItemComponent(index, item, context, onFocusForGetItemComponent),
|
||||
[getItemComponent, onFocusForGetItemComponent],
|
||||
(index: number, item: Item, context: ListContext<Context>): JSX.Element => {
|
||||
const onFocus = (e: React.FocusEvent): void => {
|
||||
// If one of the item components has been focused directly, set the focused and tabIndex state
|
||||
// and stop propagation so the ListViews onFocus doesn't also handle it.
|
||||
const key = getItemKey(item);
|
||||
setIsFocused(true);
|
||||
setTabIndexKey(key);
|
||||
e.stopPropagation();
|
||||
};
|
||||
return getItemComponent(index, item, context, onFocus);
|
||||
},
|
||||
[getItemComponent, getItemKey],
|
||||
);
|
||||
/**
|
||||
* Handles focus events on the list.
|
||||
|
||||
@@ -7,10 +7,10 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type ChangeEvent, type CSSProperties, type ReactNode } from "react";
|
||||
import { percentageOf } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type PlaybackInterface } from "../../../audio/Playback";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
import { percentageOf } from "../../../../packages/shared-components/src/utils/numbers";
|
||||
import { _t } from "../../../languageHandler";
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -7,9 +7,9 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Clock } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type IRecordingUpdate } from "../../../audio/VoiceRecording";
|
||||
import { Clock } from "../../../../packages/shared-components/src/audio/Clock";
|
||||
import { MarkedExecution } from "../../../utils/MarkedExecution";
|
||||
import { type VoiceMessageRecording } from "../../../audio/VoiceMessageRecording";
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { Clock } from "@element-hq/web-shared-components";
|
||||
|
||||
import { Clock } from "../../../../packages/shared-components/src/audio/Clock";
|
||||
import { type Playback, PlaybackState } from "../../../audio/Playback";
|
||||
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { percentageOf } from "@element-hq/web-shared-components";
|
||||
|
||||
import { arraySeed, arrayTrimFill } from "../../../utils/arrays";
|
||||
import Waveform from "./Waveform";
|
||||
import { type Playback } from "../../../audio/Playback";
|
||||
import { percentageOf } from "../../../../packages/shared-components/src/utils/numbers";
|
||||
import { PLAYBACK_WAVEFORM_SAMPLES } from "../../../audio/consts";
|
||||
|
||||
interface IProps {
|
||||
|
||||
@@ -8,7 +8,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { type HTMLProps, useContext } from "react";
|
||||
import { type Beacon, BeaconEvent, LocationAssetType } from "matrix-js-sdk/src/matrix";
|
||||
import { humanizeTime } from "@element-hq/web-shared-components";
|
||||
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import { useEventEmitterState } from "../../../hooks/useEventEmitter";
|
||||
@@ -19,6 +18,7 @@ import BeaconStatus from "./BeaconStatus";
|
||||
import { BeaconDisplayStatus } from "./displayStatus";
|
||||
import StyledLiveBeaconIcon from "./StyledLiveBeaconIcon";
|
||||
import ShareLatestLocation from "./ShareLatestLocation";
|
||||
import { humanizeTime } from "../../../../packages/shared-components/src/utils/humanize";
|
||||
|
||||
interface Props {
|
||||
beacon: Beacon;
|
||||
|
||||
@@ -735,7 +735,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||
if (this.state.reactionPickerDisplayed) {
|
||||
const buttonRect = (this.reactButtonRef.current as HTMLElement)?.getBoundingClientRect();
|
||||
reactionPicker = (
|
||||
<ContextMenu {...toRightOf(buttonRect)} onFinished={this.closeMenu} managed={false} focusLock>
|
||||
<ContextMenu {...toRightOf(buttonRect)} onFinished={this.closeMenu} managed={false}>
|
||||
<ReactionPicker mxEvent={mxEvent} onFinished={this.onCloseReactionPicker} reactions={reactions} />
|
||||
</ContextMenu>
|
||||
);
|
||||
|
||||
@@ -126,7 +126,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
const opts: IOpts = {};
|
||||
const createOpts: IOpts["createOpts"] = (opts.createOpts = {});
|
||||
opts.roomType = this.props.type;
|
||||
opts.name = this.state.name;
|
||||
createOpts.name = this.state.name;
|
||||
|
||||
if (this.state.joinRule === JoinRule.Public) {
|
||||
createOpts.visibility = Visibility.Public;
|
||||
@@ -139,7 +139,7 @@ export default class CreateRoomDialog extends React.Component<IProps, IState> {
|
||||
}
|
||||
|
||||
if (this.state.topic) {
|
||||
opts.topic = this.state.topic;
|
||||
createOpts.topic = this.state.topic;
|
||||
}
|
||||
if (this.state.noFederate) {
|
||||
createOpts.creation_content = { "m.federate": false };
|
||||
|
||||
@@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||
import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { uniqBy } from "lodash";
|
||||
import { RichList, RichItem, PillInput, Pill } from "@element-hq/web-shared-components";
|
||||
|
||||
import { Icon as EmailPillAvatarIcon } from "../../../../res/img/icon-email-pill-avatar.svg";
|
||||
import { _t, _td } from "../../../languageHandler";
|
||||
@@ -64,6 +63,10 @@ import AskInviteAnywayDialog, { type UnknownProfiles } from "./AskInviteAnywayDi
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { type UserProfilesStore } from "../../../stores/UserProfilesStore";
|
||||
import InviteProgressBody from "./InviteProgressBody.tsx";
|
||||
import { RichList } from "../../../../packages/shared-components/src/rich-list/RichList";
|
||||
import { RichItem } from "../../../../packages/shared-components/src/rich-list/RichItem";
|
||||
import { PillInput } from "../../../../packages/shared-components/src/pill-input/PillInput";
|
||||
import { Pill } from "../../../../packages/shared-components/src/pill-input/Pill";
|
||||
|
||||
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
@@ -12,8 +12,8 @@ import { debounce } from "lodash";
|
||||
import classNames from "classnames";
|
||||
import React, { type ChangeEvent, type FormEvent } from "react";
|
||||
import { type SecretStorage } from "matrix-js-sdk/src/matrix";
|
||||
import { Flex } from "@element-hq/web-shared-components";
|
||||
|
||||
import { Flex } from "../../../../../packages/shared-components/src/utils/Flex";
|
||||
import { _t } from "../../../../languageHandler";
|
||||
import { EncryptionCard } from "../../settings/encryption/EncryptionCard";
|
||||
import { EncryptionCardButtons } from "../../settings/encryption/EncryptionCardButtons";
|
||||
|
||||
@@ -6,13 +6,13 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, useCallback, useId, useState } from "react";
|
||||
import { _t } from "@element-hq/web-shared-components";
|
||||
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import { type SettingLevel } from "../../../settings/SettingLevel";
|
||||
import { SETTINGS, type StringSettingKey } from "../../../settings/Settings";
|
||||
import { useSettingValueAt } from "../../../hooks/useSettings.ts";
|
||||
import Dropdown, { type DropdownProps } from "./Dropdown.tsx";
|
||||
import { _t } from "../../../../packages/shared-components/src/utils/i18n.tsx";
|
||||
|
||||
interface Props {
|
||||
settingKey: StringSettingKey;
|
||||
|
||||
@@ -61,17 +61,17 @@ class Category extends React.PureComponent<IProps> {
|
||||
return (
|
||||
<div key={rowIndex} role="row">
|
||||
{emojisForRow.map((emoji) => (
|
||||
<div role="gridcell" className="mx_EmojiPicker_item_wrapper" key={emoji.hexcode}>
|
||||
<Emoji
|
||||
emoji={emoji}
|
||||
selectedEmojis={selectedEmojis}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
|
||||
id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
|
||||
/>
|
||||
</div>
|
||||
<Emoji
|
||||
key={emoji.hexcode}
|
||||
emoji={emoji}
|
||||
selectedEmojis={selectedEmojis}
|
||||
onClick={onClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
disabled={this.props.isEmojiDisabled?.(emoji.unicode)}
|
||||
id={`mx_EmojiPicker_item_${this.props.id}_${hexEncode(emoji.unicode)}`}
|
||||
role="gridcell"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -118,7 +118,6 @@ class Category extends React.PureComponent<IProps> {
|
||||
overflowMargin={0}
|
||||
renderItem={this.renderEmojiRow}
|
||||
role="grid"
|
||||
aria-multiselectable
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -15,17 +15,13 @@ import { RovingAccessibleButton } from "../../../accessibility/RovingTabIndex";
|
||||
|
||||
interface IProps {
|
||||
emoji: IEmoji;
|
||||
/**
|
||||
* Set of which emojis are already selected and should be decorated as such.
|
||||
* If specified, emoji will use a checkbox role with aria-checked set appropriately.
|
||||
*/
|
||||
selectedEmojis?: Set<string>;
|
||||
onClick(ev: ButtonEvent, emoji: IEmoji): void;
|
||||
onMouseEnter(emoji: IEmoji): void;
|
||||
onMouseLeave(emoji: IEmoji): void;
|
||||
disabled?: boolean;
|
||||
id?: string;
|
||||
className?: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
class Emoji extends React.PureComponent<IProps> {
|
||||
@@ -38,10 +34,9 @@ class Emoji extends React.PureComponent<IProps> {
|
||||
onClick={(ev: ButtonEvent) => onClick(ev, emoji)}
|
||||
onMouseEnter={() => onMouseEnter(emoji)}
|
||||
onMouseLeave={() => onMouseLeave(emoji)}
|
||||
className={this.props.className}
|
||||
disabled={this.props.disabled || undefined}
|
||||
role={selectedEmojis ? "checkbox" : undefined}
|
||||
aria-checked={this.props.disabled ? undefined : isSelected}
|
||||
className="mx_EmojiPicker_item_wrapper"
|
||||
disabled={this.props.disabled}
|
||||
role={this.props.role}
|
||||
focusOnMouseOver
|
||||
>
|
||||
<div className={`mx_EmojiPicker_item ${isSelected ? "mx_EmojiPicker_item_selected" : ""}`}>
|
||||
|
||||
@@ -9,8 +9,6 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import React, { type Dispatch } from "react";
|
||||
import { DATA_BY_CATEGORY, getEmojiFromUnicode, type Emoji as IEmoji } from "@matrix-org/emojibase-bindings";
|
||||
import { clamp } from "@element-hq/web-shared-components";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import * as recent from "../../../emojipicker/recent";
|
||||
@@ -28,6 +26,7 @@ import {
|
||||
Type,
|
||||
} from "../../../accessibility/RovingTabIndex";
|
||||
import { Key } from "../../../Keyboard";
|
||||
import { clamp } from "../../../../packages/shared-components/src/utils/numbers";
|
||||
import { type ButtonEvent } from "../elements/AccessibleButton";
|
||||
|
||||
export const CATEGORY_HEADER_HEIGHT = 20;
|
||||
@@ -51,8 +50,6 @@ interface IState {
|
||||
// should be enough to never have blank rows of emojis as
|
||||
// 3 rows of overflow are also rendered. The actual value is updated on scroll.
|
||||
viewportHeight: number;
|
||||
// Track if user has interacted with arrow keys or search
|
||||
showHighlight: boolean;
|
||||
}
|
||||
|
||||
class EmojiPicker extends React.Component<IProps, IState> {
|
||||
@@ -69,7 +66,6 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
filter: "",
|
||||
scrollTop: 0,
|
||||
viewportHeight: 280,
|
||||
showHighlight: false,
|
||||
};
|
||||
|
||||
// Convert recent emoji characters to emoji data, removing unknowns and duplicates
|
||||
@@ -156,42 +152,24 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
this.updateVisibility();
|
||||
};
|
||||
|
||||
// Given a roving emoji button returns the role=row element containing it
|
||||
private getRow(rovingNode?: Element): Element | undefined {
|
||||
return this.getGridcell(rovingNode)?.parentElement ?? undefined;
|
||||
}
|
||||
|
||||
// Given a roving emoji button returns the role=gridcell element containing it
|
||||
private getGridcell(rovingNode?: Element): Element | undefined {
|
||||
return rovingNode?.parentElement ?? undefined;
|
||||
}
|
||||
|
||||
// Given a role=gridcell node returns the roving emoji button contained within
|
||||
private getRovingNode(gridcellNode?: Element): Element | undefined {
|
||||
return gridcellNode?.children[0];
|
||||
}
|
||||
|
||||
private keyboardNavigation(ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void {
|
||||
const rowElement = this.getRow(state.activeNode);
|
||||
const gridcellNode = this.getGridcell(state.activeNode);
|
||||
if (!rowElement || !gridcellNode || !state.activeNode) return;
|
||||
|
||||
// Index of element within row container
|
||||
const columnIndex = Array.from(rowElement.children).indexOf(gridcellNode);
|
||||
// Index of element within the list of roving nodes
|
||||
const node = state.activeNode;
|
||||
const parent = node?.parentElement;
|
||||
if (!parent || !state.activeNode) return;
|
||||
const rowIndex = Array.from(parent.children).indexOf(node);
|
||||
const refIndex = state.nodes.indexOf(state.activeNode);
|
||||
|
||||
let focusNode: HTMLElement | undefined;
|
||||
let newRowElement: Element | undefined;
|
||||
let newParent: HTMLElement | undefined;
|
||||
switch (ev.key) {
|
||||
case Key.ARROW_LEFT:
|
||||
focusNode = state.nodes[refIndex - 1];
|
||||
newRowElement = this.getRow(focusNode);
|
||||
newParent = focusNode?.parentElement ?? undefined;
|
||||
break;
|
||||
|
||||
case Key.ARROW_RIGHT:
|
||||
focusNode = state.nodes[refIndex + 1];
|
||||
newRowElement = this.getRow(focusNode);
|
||||
newParent = focusNode?.parentElement ?? undefined;
|
||||
break;
|
||||
|
||||
case Key.ARROW_UP:
|
||||
@@ -199,30 +177,22 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
// For up/down we find the prev/next parent by inspecting the refs either side of our row
|
||||
const node =
|
||||
ev.key === Key.ARROW_UP
|
||||
? state.nodes[refIndex - columnIndex - 1]
|
||||
: state.nodes[refIndex - columnIndex + EMOJIS_PER_ROW];
|
||||
newRowElement = this.getRow(node);
|
||||
if (newRowElement) {
|
||||
const newColumnIndex = clamp(columnIndex, 0, newRowElement.children.length - 1);
|
||||
const newTarget = this.getRovingNode(newRowElement?.children[newColumnIndex]);
|
||||
focusNode = state.nodes.find((r) => r === newTarget);
|
||||
}
|
||||
? state.nodes[refIndex - rowIndex - 1]
|
||||
: state.nodes[refIndex - rowIndex + EMOJIS_PER_ROW];
|
||||
newParent = node?.parentElement ?? undefined;
|
||||
const newTarget = newParent?.children[clamp(rowIndex, 0, newParent.children.length - 1)];
|
||||
focusNode = state.nodes.find((r) => r === newTarget);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (focusNode) {
|
||||
// Only move actual DOM focus if an emoji already has focus
|
||||
// If the input has focus, keep using aria-activedescendant for virtual focus
|
||||
if (document.activeElement !== document.querySelector(".mx_EmojiPicker_search input")) {
|
||||
focusNode?.focus();
|
||||
}
|
||||
dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { node: focusNode },
|
||||
});
|
||||
|
||||
if (rowElement !== newRowElement) {
|
||||
if (parent !== newParent) {
|
||||
focusNode?.scrollIntoView({
|
||||
behavior: "auto",
|
||||
block: "center",
|
||||
@@ -237,20 +207,6 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
|
||||
private onKeyDown = (ev: React.KeyboardEvent, state: RovingState, dispatch: Dispatch<RovingAction>): void => {
|
||||
if (state.activeNode && [Key.ARROW_DOWN, Key.ARROW_RIGHT, Key.ARROW_LEFT, Key.ARROW_UP].includes(ev.key)) {
|
||||
// If highlight is not shown yet, show it and reset to first emoji
|
||||
if (!this.state.showHighlight) {
|
||||
this.setState({ showHighlight: true });
|
||||
// Reset to first emoji when showing highlight for the first time (or after it was hidden)
|
||||
if (state.nodes.length > 0) {
|
||||
dispatch({
|
||||
type: Type.SetFocus,
|
||||
payload: { node: state.nodes[0] },
|
||||
});
|
||||
}
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
return;
|
||||
}
|
||||
this.keyboardNavigation(ev, state, dispatch);
|
||||
}
|
||||
};
|
||||
@@ -292,15 +248,6 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
|
||||
private onChangeFilter = (filter: string): void => {
|
||||
const lcFilter = filter.toLowerCase().trim(); // filter is case insensitive
|
||||
|
||||
// User has typed a query, show highlight
|
||||
// If filter is cleared, hide highlight again
|
||||
if (lcFilter && !this.state.showHighlight) {
|
||||
this.setState({ showHighlight: true });
|
||||
} else if (!lcFilter && this.state.showHighlight) {
|
||||
this.setState({ showHighlight: false });
|
||||
}
|
||||
|
||||
for (const cat of this.categories) {
|
||||
let emojis: IEmoji[];
|
||||
// If the new filter string includes the old filter string, we don't have to re-filter the whole dataset.
|
||||
@@ -362,11 +309,8 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
};
|
||||
|
||||
private onEnterFilter = (): void => {
|
||||
// Only select emoji if highlight is shown
|
||||
if (!this.state.showHighlight) return;
|
||||
|
||||
const btn = this.scrollRef.current?.containerRef.current?.querySelector<HTMLButtonElement>(
|
||||
'.mx_EmojiPicker_item_wrapper [tabindex="0"]',
|
||||
'.mx_EmojiPicker_item_wrapper[tabindex="0"]',
|
||||
);
|
||||
btn?.click();
|
||||
this.props.onFinished();
|
||||
@@ -421,9 +365,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
|
||||
/>
|
||||
<AutoHideScrollbar
|
||||
id="mx_EmojiPicker_body"
|
||||
className={classNames("mx_EmojiPicker_body", {
|
||||
mx_EmojiPicker_body_showHighlight: this.state.showHighlight,
|
||||
})}
|
||||
className="mx_EmojiPicker_body"
|
||||
ref={this.scrollRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
|
||||
@@ -73,7 +73,6 @@ class QuickReactions extends React.Component<IProps, IState> {
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
selectedEmojis={this.props.selectedEmojis}
|
||||
className="mx_EmojiPicker_item_wrapper"
|
||||
/>
|
||||
))}
|
||||
</Toolbar>
|
||||
|
||||
@@ -71,9 +71,7 @@ class Search extends React.PureComponent<IProps> {
|
||||
onChange={(ev) => this.props.onChange(ev.target.value)}
|
||||
onKeyDown={this.onKeyDown}
|
||||
ref={this.inputRef}
|
||||
// Setting aria-activedescendant on the input allows screen readers to identify the active emoji.
|
||||
// Setting it when there is not a query causes screen readers to read out the first emoji when focusing the input, and it continually tells you you are in the table vs the input.
|
||||
aria-activedescendant={this.props.query ? this.context.state.activeNode?.id : undefined}
|
||||
aria-activedescendant={this.context.state.activeNode?.id}
|
||||
aria-controls="mx_EmojiPicker_body"
|
||||
aria-haspopup="grid"
|
||||
aria-autocomplete="list"
|
||||
|
||||
@@ -10,7 +10,6 @@ import React, { type JSX, createRef } from "react";
|
||||
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||
import { CallErrorCode, CallState } from "matrix-js-sdk/src/webrtc/call";
|
||||
import classNames from "classnames";
|
||||
import { Clock } from "@element-hq/web-shared-components";
|
||||
|
||||
import { _t } from "../../../languageHandler";
|
||||
import MemberAvatar from "../avatars/MemberAvatar";
|
||||
@@ -19,6 +18,7 @@ import { LegacyCallEventGrouperEvent } from "../../structures/LegacyCallEventGro
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import InfoTooltip, { InfoTooltipKind } from "../elements/InfoTooltip";
|
||||
import { formatPreciseDuration } from "../../../DateUtils";
|
||||
import { Clock } from "../../../../packages/shared-components/src/audio/Clock";
|
||||
|
||||
const MAX_NON_NARROW_WIDTH = (450 / 70) * 100;
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import React, { type JSX, useEffect, useMemo } from "react";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import { type IContent } from "matrix-js-sdk/src/matrix";
|
||||
import { type MediaEventContent } from "matrix-js-sdk/src/types";
|
||||
import { AudioPlayerView } from "@element-hq/web-shared-components";
|
||||
|
||||
import { type Playback } from "../../../audio/Playback";
|
||||
import InlineSpinner from "../elements/InlineSpinner";
|
||||
@@ -21,6 +20,7 @@ import { PlaybackManager } from "../../../audio/PlaybackManager";
|
||||
import RoomContext, { TimelineRenderingType } from "../../../contexts/RoomContext";
|
||||
import MediaProcessingError from "./shared/MediaProcessingError";
|
||||
import { AudioPlayerViewModel } from "../../../viewmodels/audio/AudioPlayerViewModel";
|
||||
import { AudioPlayerView } from "../../../../packages/shared-components/src/audio/AudioPlayerView";
|
||||
|
||||
interface IState {
|
||||
error?: boolean;
|
||||
|
||||
@@ -325,11 +325,11 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<fieldset className="mx_MPollBody">
|
||||
<legend data-testid="pollQuestion">
|
||||
<div className="mx_MPollBody">
|
||||
<h2 data-testid="pollQuestion">
|
||||
{pollEvent.question.text}
|
||||
{editedSpan}
|
||||
</legend>
|
||||
</h2>
|
||||
<div className="mx_MPollBody_allOptions">
|
||||
{pollEvent.answers.map((answer: PollAnswerSubevent) => {
|
||||
let answerVotes = 0;
|
||||
@@ -360,7 +360,7 @@ export default class MPollBody extends React.Component<IBodyProps, IState> {
|
||||
{totalText}
|
||||
{isFetchingResponses && <Spinner w={16} h={16} />}
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||