Merge remote-tracking branch 'upstream/develop' into compact-reply-rendering
|
@ -1,16 +0,0 @@
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
|
||||||
|
|
||||||
src/Markdown.js
|
|
||||||
src/NodeAnimator.js
|
|
||||||
src/components/structures/RoomDirectory.js
|
|
||||||
src/components/views/rooms/MemberList.js
|
|
||||||
src/ratelimitedfunc.js
|
|
||||||
src/utils/DMRoomMap.js
|
|
||||||
src/utils/MultiInviter.js
|
|
||||||
test/components/structures/MessagePanel-test.js
|
|
||||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
|
||||||
test/mock-clock.js
|
|
||||||
src/component-index.js
|
|
||||||
test/end-to-end-tests/node_modules/
|
|
||||||
test/end-to-end-tests/element/
|
|
||||||
test/end-to-end-tests/synapse/
|
|
63
.eslintrc.js
|
@ -1,7 +1,9 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ["matrix-org", "matrix-org/react-legacy"],
|
plugins: ["matrix-org"],
|
||||||
parser: "babel-eslint",
|
extends: [
|
||||||
|
"plugin:matrix-org/babel",
|
||||||
|
"plugin:matrix-org/react",
|
||||||
|
],
|
||||||
env: {
|
env: {
|
||||||
browser: true,
|
browser: true,
|
||||||
node: true,
|
node: true,
|
||||||
|
@ -15,35 +17,60 @@ module.exports = {
|
||||||
"prefer-promise-reject-errors": "off",
|
"prefer-promise-reject-errors": "off",
|
||||||
"no-async-promise-executor": "off",
|
"no-async-promise-executor": "off",
|
||||||
"quotes": "off",
|
"quotes": "off",
|
||||||
},
|
"no-extra-boolean-cast": "off",
|
||||||
|
|
||||||
|
// Bind or arrow functions in props causes performance issues (but we
|
||||||
|
// currently use them in some places).
|
||||||
|
// It's disabled here, but we should using it sparingly.
|
||||||
|
"react/jsx-no-bind": "off",
|
||||||
|
"react/jsx-key": ["error"],
|
||||||
|
|
||||||
|
"no-restricted-properties": [
|
||||||
|
"error",
|
||||||
|
...buildRestrictedPropertiesOptions(
|
||||||
|
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
||||||
|
"Use UIStore to access window dimensions instead.",
|
||||||
|
),
|
||||||
|
...buildRestrictedPropertiesOptions(
|
||||||
|
["*.mxcUrlToHttp", "*.getHttpUriForMxc"],
|
||||||
|
"Use Media helper instead to centralise access for customisation.",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
overrides: [{
|
overrides: [{
|
||||||
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
|
files: [
|
||||||
"extends": ["matrix-org/ts"],
|
"src/**/*.{ts,tsx}",
|
||||||
"rules": {
|
"test/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
extends: [
|
||||||
|
"plugin:matrix-org/typescript",
|
||||||
|
"plugin:matrix-org/react",
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
// Things we do that break the ideal style
|
||||||
|
"prefer-promise-reject-errors": "off",
|
||||||
|
"quotes": "off",
|
||||||
|
"no-extra-boolean-cast": "off",
|
||||||
|
|
||||||
|
// Remove Babel things manually due to override limitations
|
||||||
|
"@babel/no-invalid-this": ["off"],
|
||||||
|
|
||||||
// We're okay being explicit at the moment
|
// We're okay being explicit at the moment
|
||||||
"@typescript-eslint/no-empty-interface": "off",
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
// We disable this while we're transitioning
|
// We disable this while we're transitioning
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
// We'd rather not do this but we do
|
// We'd rather not do this but we do
|
||||||
"@typescript-eslint/ban-ts-comment": "off",
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
|
||||||
"quotes": "off",
|
|
||||||
"no-extra-boolean-cast": "off",
|
|
||||||
"no-restricted-properties": [
|
|
||||||
"error",
|
|
||||||
...buildRestrictedPropertiesOptions(
|
|
||||||
["window.innerHeight", "window.innerWidth", "window.visualViewport"],
|
|
||||||
"Use UIStore to access window dimensions instead",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
function buildRestrictedPropertiesOptions(properties, message) {
|
function buildRestrictedPropertiesOptions(properties, message) {
|
||||||
return properties.map(prop => {
|
return properties.map(prop => {
|
||||||
const [object, property] = prop.split(".");
|
let [object, property] = prop.split(".");
|
||||||
|
if (object === "*") {
|
||||||
|
object = undefined;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
object,
|
object,
|
||||||
property,
|
property,
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
[include]
|
|
||||||
src/**/*.js
|
|
||||||
test/**/*.js
|
|
||||||
|
|
||||||
[ignore]
|
|
||||||
node_modules/
|
|
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request -->
|
||||||
|
|
||||||
|
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off -->
|
7
.github/workflows/develop.yml
vendored
|
@ -11,10 +11,13 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
- name: End-to-End tests
|
- name: Prepare End-to-End tests
|
||||||
run: ./scripts/ci/end-to-end-tests.sh
|
run: ./scripts/ci/prepare-end-to-end-tests.sh
|
||||||
|
- name: Run End-to-End tests
|
||||||
|
run: ./scripts/ci/run-end-to-end-tests.sh
|
||||||
- name: Archive logs
|
- name: Archive logs
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v2
|
||||||
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
test/end-to-end-tests/logs/**/*
|
test/end-to-end-tests/logs/**/*
|
||||||
|
|
130
CHANGELOG.md
|
@ -1,3 +1,133 @@
|
||||||
|
Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21)
|
||||||
|
=====================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 12.0.0
|
||||||
|
* [Release] Keep composer reply when scrolling away from a highlighted event
|
||||||
|
[\#6211](https://github.com/matrix-org/matrix-react-sdk/pull/6211)
|
||||||
|
* [Release] Remove stray bullet point in reply preview
|
||||||
|
[\#6210](https://github.com/matrix-org/matrix-react-sdk/pull/6210)
|
||||||
|
* [Release] Stop requesting null next replies from the server
|
||||||
|
[\#6209](https://github.com/matrix-org/matrix-react-sdk/pull/6209)
|
||||||
|
|
||||||
|
Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15)
|
||||||
|
===============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade to JS SDK 12.0.0-rc.1
|
||||||
|
* Translations update from Weblate
|
||||||
|
[\#6192](https://github.com/matrix-org/matrix-react-sdk/pull/6192)
|
||||||
|
* Disable comment-on-alert for PR coming from a fork
|
||||||
|
[\#6189](https://github.com/matrix-org/matrix-react-sdk/pull/6189)
|
||||||
|
* Add JS benchmark tracking in CI
|
||||||
|
[\#6177](https://github.com/matrix-org/matrix-react-sdk/pull/6177)
|
||||||
|
* Upgrade matrix-react-test-utils for React 17 peer deps
|
||||||
|
[\#6187](https://github.com/matrix-org/matrix-react-sdk/pull/6187)
|
||||||
|
* Fix display name overlaps on the IRC layout
|
||||||
|
[\#6186](https://github.com/matrix-org/matrix-react-sdk/pull/6186)
|
||||||
|
* Small fixes to the spaces experience
|
||||||
|
[\#6184](https://github.com/matrix-org/matrix-react-sdk/pull/6184)
|
||||||
|
* Add footer and privacy note to the start dm dialog
|
||||||
|
[\#6111](https://github.com/matrix-org/matrix-react-sdk/pull/6111)
|
||||||
|
* Format mxids when disambiguation needed
|
||||||
|
[\#5880](https://github.com/matrix-org/matrix-react-sdk/pull/5880)
|
||||||
|
* Move various createRoom types to the js-sdk
|
||||||
|
[\#6183](https://github.com/matrix-org/matrix-react-sdk/pull/6183)
|
||||||
|
* Fix HTML tag for Event Tile when not rendered in a list
|
||||||
|
[\#6175](https://github.com/matrix-org/matrix-react-sdk/pull/6175)
|
||||||
|
* Remove legacy polyfills and unused dependencies
|
||||||
|
[\#6176](https://github.com/matrix-org/matrix-react-sdk/pull/6176)
|
||||||
|
* Fix buggy hovering/selecting of event tiles
|
||||||
|
[\#6173](https://github.com/matrix-org/matrix-react-sdk/pull/6173)
|
||||||
|
* Add room intro warning when e2ee is not enabled
|
||||||
|
[\#5929](https://github.com/matrix-org/matrix-react-sdk/pull/5929)
|
||||||
|
* Migrate end to end tests to GitHub actions
|
||||||
|
[\#6156](https://github.com/matrix-org/matrix-react-sdk/pull/6156)
|
||||||
|
* Fix expanding last collapsed sticky session when zoomed in
|
||||||
|
[\#6171](https://github.com/matrix-org/matrix-react-sdk/pull/6171)
|
||||||
|
* ⚛️ Upgrade to React@17
|
||||||
|
[\#6165](https://github.com/matrix-org/matrix-react-sdk/pull/6165)
|
||||||
|
* Revert refreshStickyHeaders optimisations
|
||||||
|
[\#6168](https://github.com/matrix-org/matrix-react-sdk/pull/6168)
|
||||||
|
* Add logging for which rooms calls are in
|
||||||
|
[\#6170](https://github.com/matrix-org/matrix-react-sdk/pull/6170)
|
||||||
|
* Restore read receipt animation from event to event
|
||||||
|
[\#6169](https://github.com/matrix-org/matrix-react-sdk/pull/6169)
|
||||||
|
* Restore copy button icon when sharing permalink
|
||||||
|
[\#6166](https://github.com/matrix-org/matrix-react-sdk/pull/6166)
|
||||||
|
* Restore Page Up/Down key bindings when focusing the composer
|
||||||
|
[\#6167](https://github.com/matrix-org/matrix-react-sdk/pull/6167)
|
||||||
|
* Timeline rendering optimizations
|
||||||
|
[\#6143](https://github.com/matrix-org/matrix-react-sdk/pull/6143)
|
||||||
|
* Bump css-what from 5.0.0 to 5.0.1
|
||||||
|
[\#6164](https://github.com/matrix-org/matrix-react-sdk/pull/6164)
|
||||||
|
* Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests
|
||||||
|
[\#6145](https://github.com/matrix-org/matrix-react-sdk/pull/6145)
|
||||||
|
* Bump trim-newlines from 3.0.0 to 3.0.1
|
||||||
|
[\#6163](https://github.com/matrix-org/matrix-react-sdk/pull/6163)
|
||||||
|
* Fix upgrade to element home button in top left menu
|
||||||
|
[\#6162](https://github.com/matrix-org/matrix-react-sdk/pull/6162)
|
||||||
|
* Fix unpinning of pinned messages and panel empty state
|
||||||
|
[\#6140](https://github.com/matrix-org/matrix-react-sdk/pull/6140)
|
||||||
|
* Better handling for widgets that fail to load
|
||||||
|
[\#6161](https://github.com/matrix-org/matrix-react-sdk/pull/6161)
|
||||||
|
* Improved forwarding UI
|
||||||
|
[\#5999](https://github.com/matrix-org/matrix-react-sdk/pull/5999)
|
||||||
|
* Fixes for sharing room links
|
||||||
|
[\#6118](https://github.com/matrix-org/matrix-react-sdk/pull/6118)
|
||||||
|
* Fix setting watchers
|
||||||
|
[\#6160](https://github.com/matrix-org/matrix-react-sdk/pull/6160)
|
||||||
|
* Fix Stickerpicker context menu
|
||||||
|
[\#6152](https://github.com/matrix-org/matrix-react-sdk/pull/6152)
|
||||||
|
* Add warning to private space creation flow
|
||||||
|
[\#6155](https://github.com/matrix-org/matrix-react-sdk/pull/6155)
|
||||||
|
* Add prop to alwaysShowTimestamps on TimelinePanel
|
||||||
|
[\#6159](https://github.com/matrix-org/matrix-react-sdk/pull/6159)
|
||||||
|
* Fix notif panel timestamp padding
|
||||||
|
[\#6157](https://github.com/matrix-org/matrix-react-sdk/pull/6157)
|
||||||
|
* Fixes and refactoring for the ImageView
|
||||||
|
[\#6149](https://github.com/matrix-org/matrix-react-sdk/pull/6149)
|
||||||
|
* Fix timestamps
|
||||||
|
[\#6148](https://github.com/matrix-org/matrix-react-sdk/pull/6148)
|
||||||
|
* Make it easier to pan images in the lightbox
|
||||||
|
[\#6147](https://github.com/matrix-org/matrix-react-sdk/pull/6147)
|
||||||
|
* Fix scroll token for EventTile and EventListSummary node type
|
||||||
|
[\#6154](https://github.com/matrix-org/matrix-react-sdk/pull/6154)
|
||||||
|
* Convert bunch of things to Typescript
|
||||||
|
[\#6153](https://github.com/matrix-org/matrix-react-sdk/pull/6153)
|
||||||
|
* Lint the typescript tests
|
||||||
|
[\#6142](https://github.com/matrix-org/matrix-react-sdk/pull/6142)
|
||||||
|
* Fix jumping to bottom without a highlighted event
|
||||||
|
[\#6146](https://github.com/matrix-org/matrix-react-sdk/pull/6146)
|
||||||
|
* Repair event status position in timeline
|
||||||
|
[\#6141](https://github.com/matrix-org/matrix-react-sdk/pull/6141)
|
||||||
|
* Adapt for js-sdk MatrixClient conversion to TS
|
||||||
|
[\#6132](https://github.com/matrix-org/matrix-react-sdk/pull/6132)
|
||||||
|
* Improve pinned messages in Labs
|
||||||
|
[\#6096](https://github.com/matrix-org/matrix-react-sdk/pull/6096)
|
||||||
|
* Map phone number lookup results to their native rooms
|
||||||
|
[\#6136](https://github.com/matrix-org/matrix-react-sdk/pull/6136)
|
||||||
|
* Fix mx_Event containment rules and empty read avatar row
|
||||||
|
[\#6138](https://github.com/matrix-org/matrix-react-sdk/pull/6138)
|
||||||
|
* Improve switch room rendering
|
||||||
|
[\#6079](https://github.com/matrix-org/matrix-react-sdk/pull/6079)
|
||||||
|
* Add CSS containment rules for shorter reflow operations
|
||||||
|
[\#6127](https://github.com/matrix-org/matrix-react-sdk/pull/6127)
|
||||||
|
* ignore hash/fragment when de-duplicating links for url previews
|
||||||
|
[\#6135](https://github.com/matrix-org/matrix-react-sdk/pull/6135)
|
||||||
|
* Clicking jump to bottom resets room hash
|
||||||
|
[\#5823](https://github.com/matrix-org/matrix-react-sdk/pull/5823)
|
||||||
|
* Use passive option for scroll handlers
|
||||||
|
[\#6113](https://github.com/matrix-org/matrix-react-sdk/pull/6113)
|
||||||
|
* Optimise memberSort performance for large list
|
||||||
|
[\#6130](https://github.com/matrix-org/matrix-react-sdk/pull/6130)
|
||||||
|
* Tweak event border radius to match action bar
|
||||||
|
[\#6133](https://github.com/matrix-org/matrix-react-sdk/pull/6133)
|
||||||
|
* Log when we ignore a second call in a room
|
||||||
|
[\#6131](https://github.com/matrix-org/matrix-react-sdk/pull/6131)
|
||||||
|
* Performance monitoring measurements
|
||||||
|
[\#6041](https://github.com/matrix-org/matrix-react-sdk/pull/6041)
|
||||||
|
|
||||||
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
|
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
|
||||||
=====================================================================================================
|
=====================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)
|
||||||
|
|
|
@ -10,7 +10,6 @@ module.exports = {
|
||||||
],
|
],
|
||||||
}],
|
}],
|
||||||
"@babel/preset-typescript",
|
"@babel/preset-typescript",
|
||||||
"@babel/preset-flow",
|
|
||||||
"@babel/preset-react",
|
"@babel/preset-react",
|
||||||
],
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
@ -19,7 +18,6 @@ module.exports = {
|
||||||
"@babel/plugin-proposal-numeric-separator",
|
"@babel/plugin-proposal-numeric-separator",
|
||||||
"@babel/plugin-proposal-class-properties",
|
"@babel/plugin-proposal-class-properties",
|
||||||
"@babel/plugin-proposal-object-rest-spread",
|
"@babel/plugin-proposal-object-rest-spread",
|
||||||
"@babel/plugin-transform-flow-comments",
|
|
||||||
"@babel/plugin-syntax-dynamic-import",
|
"@babel/plugin-syntax-dynamic-import",
|
||||||
"@babel/plugin-transform-runtime",
|
"@babel/plugin-transform-runtime",
|
||||||
],
|
],
|
||||||
|
|
39
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.23.0",
|
"version": "3.24.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -45,7 +45,7 @@
|
||||||
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
|
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
|
||||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||||
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
|
||||||
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
"lint:js": "eslint --max-warnings 0 src test",
|
||||||
"lint:types": "tsc --noEmit --jsx react",
|
"lint:types": "tsc --noEmit --jsx react",
|
||||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
@ -54,7 +54,9 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.12.5",
|
"@babel/runtime": "^7.12.5",
|
||||||
|
"@types/commonmark": "^0.27.4",
|
||||||
"await-lock": "^2.1.0",
|
"await-lock": "^2.1.0",
|
||||||
|
"blurhash": "^1.1.3",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"cheerio": "^1.0.0-rc.9",
|
"cheerio": "^1.0.0-rc.9",
|
||||||
|
@ -78,8 +80,8 @@
|
||||||
"katex": "^0.12.0",
|
"katex": "^0.12.0",
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "12.0.0",
|
||||||
"matrix-widget-api": "^0.1.0-beta.14",
|
"matrix-widget-api": "^0.1.0-beta.15",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
"pako": "^2.0.3",
|
"pako": "^2.0.3",
|
||||||
|
@ -89,7 +91,7 @@
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"re-resizable": "^6.9.0",
|
"re-resizable": "^6.9.0",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
"react-focus-lock": "^2.5.0",
|
"react-focus-lock": "^2.5.0",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
|
@ -104,16 +106,16 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.12.10",
|
"@babel/cli": "^7.12.10",
|
||||||
"@babel/core": "^7.12.10",
|
"@babel/core": "^7.12.10",
|
||||||
|
"@babel/eslint-parser": "^7.12.10",
|
||||||
|
"@babel/eslint-plugin": "^7.12.10",
|
||||||
"@babel/parser": "^7.12.11",
|
"@babel/parser": "^7.12.11",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||||
"@babel/plugin-proposal-decorators": "^7.12.12",
|
"@babel/plugin-proposal-decorators": "^7.12.12",
|
||||||
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
"@babel/plugin-proposal-export-default-from": "^7.12.1",
|
||||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||||
"@babel/plugin-transform-flow-comments": "^7.12.1",
|
|
||||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||||
"@babel/preset-env": "^7.12.11",
|
"@babel/preset-env": "^7.12.11",
|
||||||
"@babel/preset-flow": "^7.12.1",
|
|
||||||
"@babel/preset-react": "^7.12.10",
|
"@babel/preset-react": "^7.12.10",
|
||||||
"@babel/preset-typescript": "^7.12.7",
|
"@babel/preset-typescript": "^7.12.7",
|
||||||
"@babel/register": "^7.12.10",
|
"@babel/register": "^7.12.10",
|
||||||
|
@ -123,6 +125,7 @@
|
||||||
"@sinonjs/fake-timers": "^7.0.2",
|
"@sinonjs/fake-timers": "^7.0.2",
|
||||||
"@types/classnames": "^2.2.11",
|
"@types/classnames": "^2.2.11",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
|
"@types/diff-match-patch": "^1.0.32",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/linkifyjs": "^2.1.3",
|
"@types/linkifyjs": "^2.1.3",
|
||||||
|
@ -132,23 +135,22 @@
|
||||||
"@types/pako": "^1.0.1",
|
"@types/pako": "^1.0.1",
|
||||||
"@types/parse5": "^6.0.0",
|
"@types/parse5": "^6.0.0",
|
||||||
"@types/qrcode": "^1.3.5",
|
"@types/qrcode": "^1.3.5",
|
||||||
"@types/react": "^16.9",
|
"@types/react": "^17.0.2",
|
||||||
"@types/react-dom": "^16.9.10",
|
"@types/react-beautiful-dnd": "^13.0.0",
|
||||||
|
"@types/react-dom": "^17.0.2",
|
||||||
"@types/react-transition-group": "^4.4.0",
|
"@types/react-transition-group": "^4.4.0",
|
||||||
"@types/sanitize-html": "^2.3.1",
|
"@types/sanitize-html": "^2.3.1",
|
||||||
"@types/zxcvbn": "^4.4.0",
|
"@types/zxcvbn": "^4.4.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.14.0",
|
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||||
"@typescript-eslint/parser": "^4.14.0",
|
"@typescript-eslint/parser": "^4.17.0",
|
||||||
"babel-eslint": "^10.1.0",
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
"babel-jest": "^26.6.3",
|
"babel-jest": "^26.6.3",
|
||||||
"chokidar": "^3.5.1",
|
"chokidar": "^3.5.1",
|
||||||
"concurrently": "^5.3.0",
|
"concurrently": "^5.3.0",
|
||||||
"enzyme": "^3.11.0",
|
"enzyme": "^3.11.0",
|
||||||
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
|
||||||
"eslint": "7.18.0",
|
"eslint": "7.18.0",
|
||||||
"eslint-config-matrix-org": "^0.2.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-plugin-babel": "^5.3.1",
|
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
|
||||||
"eslint-plugin-flowtype": "^5.2.0",
|
|
||||||
"eslint-plugin-react": "^7.22.0",
|
"eslint-plugin-react": "^7.22.0",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"glob": "^7.1.6",
|
"glob": "^7.1.6",
|
||||||
|
@ -167,13 +169,10 @@
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
"walk": "^2.3.14"
|
"walk": "^2.3.14"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
|
||||||
"**/@types/react": "^16.14"
|
|
||||||
},
|
|
||||||
"jest": {
|
"jest": {
|
||||||
"testEnvironment": "./__test-utils__/environment.js",
|
"testEnvironment": "./__test-utils__/environment.js",
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
"<rootDir>/test/**/*-test.[jt]s"
|
"<rootDir>/test/**/*-test.[jt]s?(x)"
|
||||||
],
|
],
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
"jest-canvas-mock"
|
"jest-canvas-mock"
|
||||||
|
|
|
@ -37,6 +37,11 @@
|
||||||
@import "./structures/_ViewSource.scss";
|
@import "./structures/_ViewSource.scss";
|
||||||
@import "./structures/auth/_CompleteSecurity.scss";
|
@import "./structures/auth/_CompleteSecurity.scss";
|
||||||
@import "./structures/auth/_Login.scss";
|
@import "./structures/auth/_Login.scss";
|
||||||
|
@import "./views/audio_messages/_AudioPlayer.scss";
|
||||||
|
@import "./views/audio_messages/_PlayPauseButton.scss";
|
||||||
|
@import "./views/audio_messages/_PlaybackContainer.scss";
|
||||||
|
@import "./views/audio_messages/_SeekBar.scss";
|
||||||
|
@import "./views/audio_messages/_Waveform.scss";
|
||||||
@import "./views/auth/_AuthBody.scss";
|
@import "./views/auth/_AuthBody.scss";
|
||||||
@import "./views/auth/_AuthButtons.scss";
|
@import "./views/auth/_AuthButtons.scss";
|
||||||
@import "./views/auth/_AuthFooter.scss";
|
@import "./views/auth/_AuthFooter.scss";
|
||||||
|
@ -123,7 +128,6 @@
|
||||||
@import "./views/elements/_EventListSummary.scss";
|
@import "./views/elements/_EventListSummary.scss";
|
||||||
@import "./views/elements/_FacePile.scss";
|
@import "./views/elements/_FacePile.scss";
|
||||||
@import "./views/elements/_Field.scss";
|
@import "./views/elements/_Field.scss";
|
||||||
@import "./views/elements/_FormButton.scss";
|
|
||||||
@import "./views/elements/_ImageView.scss";
|
@import "./views/elements/_ImageView.scss";
|
||||||
@import "./views/elements/_InfoTooltip.scss";
|
@import "./views/elements/_InfoTooltip.scss";
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
|
@ -167,6 +171,7 @@
|
||||||
@import "./views/messages/_MTextBody.scss";
|
@import "./views/messages/_MTextBody.scss";
|
||||||
@import "./views/messages/_MVideoBody.scss";
|
@import "./views/messages/_MVideoBody.scss";
|
||||||
@import "./views/messages/_MVoiceMessageBody.scss";
|
@import "./views/messages/_MVoiceMessageBody.scss";
|
||||||
|
@import "./views/messages/_MediaBody.scss";
|
||||||
@import "./views/messages/_MessageActionBar.scss";
|
@import "./views/messages/_MessageActionBar.scss";
|
||||||
@import "./views/messages/_MessageTimestamp.scss";
|
@import "./views/messages/_MessageTimestamp.scss";
|
||||||
@import "./views/messages/_MjolnirBody.scss";
|
@import "./views/messages/_MjolnirBody.scss";
|
||||||
|
@ -256,9 +261,6 @@
|
||||||
@import "./views/toasts/_AnalyticsToast.scss";
|
@import "./views/toasts/_AnalyticsToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voice_messages/_PlayPauseButton.scss";
|
|
||||||
@import "./views/voice_messages/_PlaybackContainer.scss";
|
|
||||||
@import "./views/voice_messages/_Waveform.scss";
|
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_CallViewForRoom.scss";
|
@import "./views/voip/_CallViewForRoom.scss";
|
||||||
|
|
|
@ -323,7 +323,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_GroupView_featuredThing .mx_BaseAvatar {
|
.mx_GroupView_featuredThing .mx_BaseAvatar {
|
||||||
/* To prevent misalignment with mx_TintableSvg (in addButton) */
|
/* To prevent misalignment with img (in addButton) */
|
||||||
vertical-align: initial;
|
vertical-align: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -111,6 +111,29 @@ $roomListCollapsedWidth: 68px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanel_dialPadButton {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $roomlist-button-bg-color;
|
||||||
|
position: relative;
|
||||||
|
margin-left: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
mask-image: url('$(res)/img/element-icons/call/dialpad.svg');
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_exploreButton {
|
.mx_LeftPanel_exploreButton {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
@ -185,6 +208,12 @@ $roomListCollapsedWidth: 68px;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
|
.mx_LeftPanel_dialPadButton {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_exploreButton {
|
.mx_LeftPanel_exploreButton {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
|
|
|
@ -112,7 +112,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding
|
padding-left: 30px; // 18px for the icon, 2px margin to text, 10px regular padding
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
@ -128,13 +128,14 @@ limitations under the License.
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
top: 50%; // text sizes are dynamic
|
||||||
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomStatusBar_unsentCancelAllBtn::before {
|
&.mx_RoomStatusBar_unsentCancelAllBtn::before {
|
||||||
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
width: 12px;
|
|
||||||
height: 16px;
|
|
||||||
top: calc(50% - 8px); // text sizes are dynamic
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomStatusBar_unsentResendAllBtn {
|
&.mx_RoomStatusBar_unsentResendAllBtn {
|
||||||
|
@ -142,9 +143,6 @@ limitations under the License.
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/retry.svg');
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
top: calc(50% - 9px); // text sizes are dynamic
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
// Create another flexbox so the Panel fills the container
|
// Create another flexbox so the Panel fills the container
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.mx_SpacePanel_spaceTreeWrapper {
|
.mx_SpacePanel_spaceTreeWrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_SpaceItem_dragging {
|
||||||
|
.mx_SpaceButton_toggleCollapse {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_SpaceTreeLevel {
|
.mx_SpaceTreeLevel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -71,7 +71,7 @@ limitations under the License.
|
||||||
&::before {
|
&::before {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::after {
|
&::after {
|
||||||
|
@ -135,10 +135,14 @@ limitations under the License.
|
||||||
float: right;
|
float: right;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.mx_FormButton {
|
.mx_AccessibleButton {
|
||||||
min-width: 96px;
|
min-width: 96px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton + .mx_AccessibleButton {
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Toast_description {
|
.mx_Toast_description {
|
||||||
|
|
68
res/css/views/audio_messages/_AudioPlayer.scss
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_AudioPlayer_container {
|
||||||
|
padding: 16px 12px 12px 12px;
|
||||||
|
max-width: 267px; // use max to make the control fit in the files/pinned panels
|
||||||
|
|
||||||
|
.mx_AudioPlayer_primaryContainer {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.mx_PlayPauseButton {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AudioPlayer_mediaInfo {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden; // makes the ellipsis on the file name work
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AudioPlayer_mediaName {
|
||||||
|
color: $primary-fg-color;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-bottom: 4px; // mimics the line-height differences in the Figma
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AudioPlayer_byline {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AudioPlayer_seek {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.mx_SeekBar {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Clock {
|
||||||
|
width: $font-42px; // we're not using a monospace font, so fake it
|
||||||
|
min-width: $font-42px; // for flexbox
|
||||||
|
padding-left: 4px; // isolate from seek bar
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
min-width: 32px; // for when the button is used in a flexbox
|
||||||
|
min-height: 32px; // for when the button is used in a flexbox
|
||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
background-color: $voice-playback-button-bg-color;
|
background-color: $voice-playback-button-bg-color;
|
||||||
|
|
|
@ -22,25 +22,24 @@ limitations under the License.
|
||||||
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
|
||||||
// has a 1px padding on it that we want to account for.
|
// has a 1px padding on it that we want to account for.
|
||||||
padding: 7px 12px 7px 11px;
|
padding: 7px 12px 7px 11px;
|
||||||
background-color: $voice-record-waveform-bg-color;
|
|
||||||
border-radius: 12px;
|
|
||||||
|
|
||||||
// Cheat at alignment a bit
|
// Cheat at alignment a bit
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
color: $voice-record-waveform-fg-color;
|
contain: content;
|
||||||
font-size: $font-14px;
|
|
||||||
line-height: $font-24px;
|
|
||||||
|
|
||||||
.mx_Waveform {
|
.mx_Waveform {
|
||||||
.mx_Waveform_bar {
|
.mx_Waveform_bar {
|
||||||
background-color: $voice-record-waveform-incomplete-fg-color;
|
background-color: $voice-record-waveform-incomplete-fg-color;
|
||||||
|
height: 100%;
|
||||||
|
/* Variable set by a JS component */
|
||||||
|
transform: scaleY(max(0.05, var(--barHeight)));
|
||||||
|
|
||||||
&.mx_Waveform_bar_100pct {
|
&.mx_Waveform_bar_100pct {
|
||||||
// Small animation to remove the mechanical feel of progress
|
// Small animation to remove the mechanical feel of progress
|
||||||
transition: background-color 250ms ease;
|
transition: background-color 250ms ease;
|
||||||
background-color: $voice-record-waveform-fg-color;
|
background-color: $message-body-panel-fg-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
103
res/css/views/audio_messages/_SeekBar.scss
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// CSS inspiration from:
|
||||||
|
// * https://www.w3schools.com/howto/howto_js_rangeslider.asp
|
||||||
|
// * https://stackoverflow.com/a/28283806
|
||||||
|
// * https://css-tricks.com/styling-cross-browser-compatible-range-inputs-css/
|
||||||
|
|
||||||
|
.mx_SeekBar {
|
||||||
|
// Dev note: we deliberately do not have the -ms-track (and friends) selectors because we don't
|
||||||
|
// need to support IE.
|
||||||
|
|
||||||
|
appearance: none; // default style override
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
background: $quaternary-fg-color;
|
||||||
|
outline: none; // remove blue selection border
|
||||||
|
position: relative; // for before+after pseudo elements later on
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
appearance: none; // default style override
|
||||||
|
|
||||||
|
// Dev note: This needs to be duplicated with the -moz-range-thumb selector
|
||||||
|
// because otherwise Edge (webkit) will fail to see the styles and just refuse
|
||||||
|
// to apply them.
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
// Firefox adds a border on the thumb
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is for webkit support, but we can't limit the functionality of it to just webkit
|
||||||
|
// browsers. Firefox responds to webkit-prefixed values now, which means we can't use media
|
||||||
|
// or support queries to selectively apply the rule. An upside is that this CSS doesn't work
|
||||||
|
// in firefox, so it's just wasted CPU/GPU time.
|
||||||
|
&::before { // ::before to ensure it ends up under the thumb
|
||||||
|
content: '';
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
|
||||||
|
// Absolute positioning to ensure it overlaps with the existing bar
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
// Sizing to match the bar
|
||||||
|
width: 100%;
|
||||||
|
height: 1px;
|
||||||
|
|
||||||
|
// And finally dynamic width without overly hurting the rendering engine.
|
||||||
|
transform-origin: 0 100%;
|
||||||
|
transform: scaleX(var(--fillTo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is firefox's built-in support for the above, with 100% less hacks.
|
||||||
|
&::-moz-range-progress {
|
||||||
|
background-color: $tertiary-fg-color;
|
||||||
|
height: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase clickable area for the slider (approximately same size as browser default)
|
||||||
|
// We do it this way to keep the same padding and margins of the element, avoiding margin math.
|
||||||
|
// Source: https://front-back.com/expand-clickable-areas-for-a-better-touch-experience/
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
bottom: -6px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,49 +19,68 @@ limitations under the License.
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background-color: $settings-profile-placeholder-bg-color;
|
background-color: $settings-profile-placeholder-bg-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
> div {
|
.mx_BetaCard_columns {
|
||||||
.mx_BetaCard_title {
|
display: flex;
|
||||||
font-weight: $font-semi-bold;
|
|
||||||
font-size: $font-18px;
|
|
||||||
line-height: $font-22px;
|
|
||||||
color: $primary-fg-color;
|
|
||||||
margin: 4px 0 14px;
|
|
||||||
|
|
||||||
.mx_BetaCard_betaPill {
|
> div {
|
||||||
margin-left: 12px;
|
.mx_BetaCard_title {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
font-size: $font-18px;
|
||||||
|
line-height: $font-22px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
margin: 4px 0 14px;
|
||||||
|
|
||||||
|
.mx_BetaCard_betaPill {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BetaCard_caption {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-20px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BetaCard_buttons .mx_AccessibleButton {
|
||||||
|
display: block;
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 7px 40px;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_BetaCard_disclaimer {
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_BetaCard_caption {
|
> img {
|
||||||
font-size: $font-15px;
|
margin: auto 0 auto 20px;
|
||||||
line-height: $font-20px;
|
width: 300px;
|
||||||
color: $secondary-fg-color;
|
object-fit: contain;
|
||||||
margin-bottom: 20px;
|
height: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
|
||||||
display: block;
|
|
||||||
margin: 12px 0;
|
|
||||||
padding: 7px 40px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_BetaCard_disclaimer {
|
|
||||||
font-size: $font-12px;
|
|
||||||
line-height: $font-15px;
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> img {
|
.mx_BetaCard_relatedSettings {
|
||||||
margin: auto 0 auto 20px;
|
.mx_SettingsFlag {
|
||||||
width: 300px;
|
margin: 16px 0 0;
|
||||||
object-fit: contain;
|
font-size: $font-15px;
|
||||||
height: 100%;
|
line-height: $font-24px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
|
||||||
|
.mx_SettingsFlag_microcopy {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2021 Michael Weimann <mail@michael-weimann.eu>
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,16 +16,69 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MessageContextMenu {
|
.mx_MessageContextMenu {
|
||||||
padding: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MessageContextMenu_field {
|
.mx_IconizedContextMenu_icon {
|
||||||
display: block;
|
width: 16px;
|
||||||
padding: 3px 6px 3px 6px;
|
height: 16px;
|
||||||
cursor: pointer;
|
display: block;
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MessageContextMenu_field.mx_MessageContextMenu_fieldSet {
|
&::before {
|
||||||
font-weight: bold;
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
display: block;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background: $primary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconCollapse::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/message/chevron-up.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconReport::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/warning-badge.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconLink::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/link.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconPermalink::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/share.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconUnhidePreview::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings/appearance.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconForward::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/message/fwd.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconRedact::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/trashcan.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconResend::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/retry.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconSource::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/format-bar/code.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconQuote::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/format-bar/quote.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconPin::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MessageContextMenu_iconUnpin::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/pin.svg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,6 +38,15 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/view-community.svg');
|
mask-image: url('$(res)/img/element-icons/view-community.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TagTileContextMenu_moveUp::before {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_TagTileContextMenu_moveDown::before {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
.mx_TagTileContextMenu_hideCommunity::before {
|
.mx_TagTileContextMenu_hideCommunity::before {
|
||||||
mask-image: url('$(res)/img/element-icons/hide.svg');
|
mask-image: url('$(res)/img/element-icons/hide.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ limitations under the License.
|
||||||
> .mx_ForwardDialog_preview {
|
> .mx_ForwardDialog_preview {
|
||||||
max-height: 30%;
|
max-height: 30%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: scroll;
|
overflow-y: auto;
|
||||||
|
|
||||||
div {
|
div {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -295,6 +295,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_InviteDialog_content {
|
.mx_InviteDialog_content {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -316,3 +317,42 @@ limitations under the License.
|
||||||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError {
|
||||||
|
> h4 {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
> div {
|
||||||
|
.mx_InviteDialog_multiInviterError_entry {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_userProfile {
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_name {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_userId {
|
||||||
|
margin-left: 6px;
|
||||||
|
font-size: $font-12px;
|
||||||
|
line-height: $font-15px;
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_multiInviterError_entry_error {
|
||||||
|
margin-left: 32px;
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
color: $notice-primary-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Not actually a component but things shared by settings components
|
// Not actually a component but things shared by settings components
|
||||||
.mx_UserSettingsDialog, .mx_RoomSettingsDialog {
|
.mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog {
|
||||||
width: 90vw;
|
width: 90vw;
|
||||||
max-width: 1000px;
|
max-width: 1000px;
|
||||||
// set the height too since tabbed view scrolls itself.
|
// set the height too since tabbed view scrolls itself.
|
||||||
|
|
|
@ -15,7 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_SpaceSettingsDialog {
|
.mx_SpaceSettingsDialog {
|
||||||
width: 480px;
|
|
||||||
color: $primary-fg-color;
|
color: $primary-fg-color;
|
||||||
|
|
||||||
.mx_SpaceSettings_errorText {
|
.mx_SpaceSettings_errorText {
|
||||||
|
@ -32,8 +31,44 @@ limitations under the License.
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton_kind_danger {
|
.mx_SettingsTab_section {
|
||||||
margin-top: 28px;
|
.mx_SettingsTab_section_caption {
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_SettingsTab_subheading {
|
||||||
|
border-top: 1px solid $message-body-panel-bg-color;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton {
|
||||||
|
margin-top: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.mx_RadioButton_content {
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + span {
|
||||||
|
font-size: $font-15px;
|
||||||
|
line-height: $font-18px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
margin-left: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsTab_showAdvanced {
|
||||||
|
margin: 16px 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SettingsFlag {
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceSettingsDialog_buttons {
|
.mx_SpaceSettingsDialog_buttons {
|
||||||
|
@ -52,4 +87,14 @@ limitations under the License.
|
||||||
.mx_AccessibleButton_hasKind {
|
.mx_AccessibleButton_hasKind {
|
||||||
padding: 8px 22px;
|
padding: 8px 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_TabbedView_tabLabel {
|
||||||
|
.mx_SpaceSettingsDialog_generalIcon::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_SpaceSettingsDialog_visibilityIcon::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/eye.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_FormButton {
|
|
||||||
line-height: $font-16px;
|
|
||||||
padding: 5px 15px;
|
|
||||||
font-size: $font-12px;
|
|
||||||
height: min-content;
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_kind_primary {
|
|
||||||
color: $accent-color;
|
|
||||||
background-color: $accent-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_kind_danger {
|
|
||||||
color: $notice-primary-color;
|
|
||||||
background-color: $notice-primary-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_kind_secondary {
|
|
||||||
color: $secondary-fg-color;
|
|
||||||
border: 1px solid $secondary-fg-color;
|
|
||||||
background-color: unset;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$timelineImageBorderRadius: 4px;
|
||||||
|
|
||||||
.mx_MImageBody {
|
.mx_MImageBody {
|
||||||
display: block;
|
display: block;
|
||||||
margin-right: 34px;
|
margin-right: 34px;
|
||||||
|
@ -25,7 +27,11 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
border-radius: 4px;
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
|
||||||
|
> canvas {
|
||||||
|
border-radius: $timelineImageBorderRadius;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MImageBody_thumbnail_container {
|
.mx_MImageBody_thumbnail_container {
|
||||||
|
@ -43,7 +49,7 @@ limitations under the License.
|
||||||
top: 50%;
|
top: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inner img and TintableSvg should be centered around 0, 0
|
// Inner img should be centered around 0, 0
|
||||||
.mx_MImageBody_thumbnail_spinner > * {
|
.mx_MImageBody_thumbnail_spinner > * {
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
28
res/css/views/messages/_MediaBody.scss
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// A "media body" is any file upload looking thing, apart from images and videos (they
|
||||||
|
// have unique styles).
|
||||||
|
|
||||||
|
.mx_MediaBody {
|
||||||
|
background-color: $message-body-panel-bg-color;
|
||||||
|
border-radius: 12px;
|
||||||
|
|
||||||
|
color: $message-body-panel-fg-color;
|
||||||
|
font-size: $font-14px;
|
||||||
|
line-height: $font-24px;
|
||||||
|
}
|
||||||
|
|
|
@ -17,4 +17,9 @@ limitations under the License.
|
||||||
.mx_TextualEvent {
|
.mx_TextualEvent {
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $accent-color;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_cryptoEvent_icon::after {
|
&.mx_cryptoEvent_icon::after {
|
||||||
|
@ -48,6 +48,7 @@ limitations under the License.
|
||||||
.mx_cryptoEvent_buttons {
|
.mx_cryptoEvent_buttons {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_cryptoEvent_state {
|
.mx_cryptoEvent_state {
|
||||||
|
|
|
@ -259,16 +259,6 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_AccessibleButton.mx_AccessibleButton_hasKind {
|
.mx_AccessibleButton.mx_AccessibleButton_hasKind {
|
||||||
padding: 8px 18px;
|
padding: 8px 18px;
|
||||||
|
|
||||||
&.mx_AccessibleButton_kind_primary {
|
|
||||||
color: $accent-color;
|
|
||||||
background-color: $accent-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_kind_danger {
|
|
||||||
color: $notice-primary-color;
|
|
||||||
background-color: $notice-primary-bg-color;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VerificationShowSas .mx_AccessibleButton,
|
.mx_VerificationShowSas .mx_AccessibleButton,
|
||||||
|
|
|
@ -58,7 +58,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VerificationPanel_reciprocate_section {
|
.mx_VerificationPanel_reciprocate_section {
|
||||||
.mx_FormButton {
|
.mx_AccessibleButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
|
|
|
@ -45,7 +45,7 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
// transparent-looking border surrounding the shield for when overlain over avatars
|
// transparent-looking border surrounding the shield for when overlain over avatars
|
||||||
|
@ -59,7 +59,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
// shrink the infill of the badge
|
// shrink the infill of the badge
|
||||||
&::before {
|
&::before {
|
||||||
mask-size: 65%;
|
mask-size: 60%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -345,7 +345,7 @@ $hover-select-border: 4px;
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: 90%;
|
mask-size: 80%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ $irc-line-height: $font-18px;
|
||||||
// timestamps are links which shouldn't be underlined
|
// timestamps are links which shouldn't be underlined
|
||||||
> a {
|
> a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
min-width: 45px;
|
||||||
}
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -49,18 +50,6 @@ $irc-line-height: $font-18px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .mx_SenderProfile {
|
|
||||||
order: 2;
|
|
||||||
flex-shrink: 0;
|
|
||||||
width: var(--name-width);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-align: left;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow: visible;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile_line, .mx_EventTile_reply {
|
.mx_EventTile_line, .mx_EventTile_reply {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -173,27 +162,37 @@ $irc-line-height: $font-18px;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SenderProfile_hover {
|
.mx_SenderProfile {
|
||||||
background-color: $primary-bg-color;
|
width: var(--name-width);
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
order: 2;
|
||||||
|
flex-shrink: 0;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
> .mx_SenderProfile_displayName {
|
> .mx_SenderProfile_displayName {
|
||||||
|
width: 100%;
|
||||||
|
text-align: end;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
min-width: var(--name-width);
|
}
|
||||||
text-align: end;
|
|
||||||
|
> .mx_SenderProfile_mxid {
|
||||||
|
visibility: collapse;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SenderProfile:hover {
|
.mx_SenderProfile:hover {
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SenderProfile_hover:hover {
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
width: max(auto, 100%);
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
||||||
|
> .mx_SenderProfile_displayName {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
> .mx_SenderProfile_mxid {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_ReplyThread {
|
.mx_ReplyThread {
|
||||||
|
@ -201,16 +200,7 @@ $irc-line-height: $font-18px;
|
||||||
.mx_SenderProfile {
|
.mx_SenderProfile {
|
||||||
width: unset;
|
width: unset;
|
||||||
max-width: var(--name-width);
|
max-width: var(--name-width);
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SenderProfile_hover {
|
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
> span {
|
|
||||||
> .mx_SenderProfile_displayName {
|
|
||||||
min-width: inherit;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_emote {
|
.mx_EventTile_emote {
|
||||||
|
|
|
@ -36,10 +36,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VoiceRecordComposerTile_delete {
|
.mx_VoiceRecordComposerTile_delete {
|
||||||
width: 14px; // w&h are size of icon
|
width: 24px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 11px; // distance from left edge of waveform container (container has some margin too)
|
margin-right: 8px; // distance from left edge of waveform container (container has some margin too)
|
||||||
background-color: $voice-record-icon-color;
|
background-color: $voice-record-icon-color;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_SpaceBasicSettings {
|
.mx_SpaceBasicSettings {
|
||||||
.mx_Field {
|
.mx_Field {
|
||||||
margin: 32px 0;
|
margin: 24px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_SpaceBasicSettings_avatarContainer {
|
.mx_SpaceBasicSettings_avatarContainer {
|
||||||
|
@ -73,7 +73,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_FormButton {
|
.mx_AccessibleButton_hasKind {
|
||||||
padding: 8px 22px;
|
padding: 8px 22px;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: block;
|
display: block;
|
||||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
||||||
.mx_DialPad_button {
|
.mx_DialPad_button {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
background-color: $theme-button-bg-color;
|
background-color: $dialpad-button-bg-color;
|
||||||
border-radius: 40px;
|
border-radius: 40px;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
|
@ -27,9 +27,22 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_dialled {
|
.mx_DialPadContextMenu_dialled {
|
||||||
height: 1em;
|
height: 1.5em;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
max-width: 150px;
|
||||||
|
border: none;
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
|
.mx_DialPadContextMenu_dialled input {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 150px;
|
||||||
|
text-align: left;
|
||||||
|
direction: rtl;
|
||||||
|
padding: 8px 0px;
|
||||||
|
background-color: rgb(0, 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DialPadContextMenu_dialPad {
|
.mx_DialPadContextMenu_dialPad {
|
||||||
|
|
3
res/img/element-icons/call/dialpad.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="12" height="18" viewBox="0 0 12 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M6 14.25C5.175 14.25 4.5 14.925 4.5 15.75C4.5 16.575 5.175 17.25 6 17.25C6.825 17.25 7.5 16.575 7.5 15.75C7.5 14.925 6.825 14.25 6 14.25ZM1.5 0.75C0.675 0.75 0 1.425 0 2.25C0 3.075 0.675 3.75 1.5 3.75C2.325 3.75 3 3.075 3 2.25C3 1.425 2.325 0.75 1.5 0.75ZM1.5 5.25C0.675 5.25 0 5.925 0 6.75C0 7.575 0.675 8.25 1.5 8.25C2.325 8.25 3 7.575 3 6.75C3 5.925 2.325 5.25 1.5 5.25ZM1.5 9.75C0.675 9.75 0 10.425 0 11.25C0 12.075 0.675 12.75 1.5 12.75C2.325 12.75 3 12.075 3 11.25C3 10.425 2.325 9.75 1.5 9.75ZM10.5 3.75C11.325 3.75 12 3.075 12 2.25C12 1.425 11.325 0.75 10.5 0.75C9.675 0.75 9 1.425 9 2.25C9 3.075 9.675 3.75 10.5 3.75ZM6 9.75C5.175 9.75 4.5 10.425 4.5 11.25C4.5 12.075 5.175 12.75 6 12.75C6.825 12.75 7.5 12.075 7.5 11.25C7.5 10.425 6.825 9.75 6 9.75ZM10.5 9.75C9.675 9.75 9 10.425 9 11.25C9 12.075 9.675 12.75 10.5 12.75C11.325 12.75 12 12.075 12 11.25C12 10.425 11.325 9.75 10.5 9.75ZM10.5 5.25C9.675 5.25 9 5.925 9 6.75C9 7.575 9.675 8.25 10.5 8.25C11.325 8.25 12 7.575 12 6.75C12 5.925 11.325 5.25 10.5 5.25ZM6 5.25C5.175 5.25 4.5 5.925 4.5 6.75C4.5 7.575 5.175 8.25 6 8.25C6.825 8.25 7.5 7.575 7.5 6.75C7.5 5.925 6.825 5.25 6 5.25ZM6 0.75C5.175 0.75 4.5 1.425 4.5 2.25C4.5 3.075 5.175 3.75 6 3.75C6.825 3.75 7.5 3.075 7.5 2.25C7.5 1.425 6.825 0.75 6 0.75Z" fill="#737D8C"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
3
res/img/element-icons/eye.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3094 5.96587C15.3206 7.15704 15.3417 8.85457 14.3412 10.0548C13.0889 11.5571 10.9822 13.3332 8.02104 13.3332C5.05992 13.3332 2.9532 11.5571 1.70087 10.0548C0.700398 8.85457 0.721506 7.15704 1.7327 5.96587C3.01174 4.45918 5.1391 2.6665 8.02104 2.6665C10.903 2.6665 13.0303 4.45918 14.3094 5.96587ZM11.5556 7.99984C11.5556 9.96352 9.96369 11.5554 8.00001 11.5554C6.03633 11.5554 4.44446 9.96352 4.44446 7.99984C4.44446 6.03616 6.03633 4.44428 8.00001 4.44428C9.96369 4.44428 11.5556 6.03616 11.5556 7.99984ZM8.00001 9.77761C8.98185 9.77761 9.77779 8.98168 9.77779 7.99984C9.77779 7.018 8.98185 6.22206 8.00001 6.22206C7.01817 6.22206 6.22224 7.018 6.22224 7.99984C6.22224 8.98168 7.01817 9.77761 8.00001 9.77761Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 887 B |
1
res/img/element-icons/message/chevron-up.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-up"><polyline points="18 15 12 9 6 15"></polyline></svg>
|
After Width: | Height: | Size: 268 B |
1
res/img/element-icons/message/corner-up-right.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-corner-up-right"><polyline points="15 14 20 9 15 4"></polyline><path d="M4 20v-7a4 4 0 0 1 4-4h12"></path></svg>
|
After Width: | Height: | Size: 316 B |
3
res/img/element-icons/message/fwd.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.9454 4.27941C10.653 3.98601 10.6539 3.51114 10.9472 3.21875C11.2406 2.92637 11.7155 2.92719 12.0079 3.22059L15.5312 6.75612C15.8229 7.0488 15.8229 7.52226 15.5312 7.81494L12.0079 11.3505C11.7155 11.6439 11.2407 11.6447 10.9473 11.3523C10.6539 11.06 10.653 10.5851 10.9454 10.2917L13.2292 8H6.36588C4.95064 8 3.75282 9.20272 3.75282 10.75C3.75282 12.2973 4.95064 13.5 6.36588 13.5H7.93524C8.34945 13.5 8.68524 13.8358 8.68524 14.25C8.68524 14.6642 8.34945 15 7.93524 15H6.36588C4.06634 15 2.25282 13.0687 2.25282 10.75C2.25282 8.43128 4.06634 6.5 6.36588 6.5H13.1583L10.9454 4.27941Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 755 B |
1
res/img/element-icons/message/link.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
|
After Width: | Height: | Size: 371 B |
1
res/img/element-icons/message/repeat.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-repeat"><polyline points="17 1 21 5 17 9"></polyline><path d="M3 11V9a4 4 0 0 1 4-4h14"></path><polyline points="7 23 3 19 7 15"></polyline><path d="M21 13v2a4 4 0 0 1-4 4H3"></path></svg>
|
After Width: | Height: | Size: 392 B |
1
res/img/element-icons/message/share.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-share"><path d="M4 12v8a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-8"></path><polyline points="16 6 12 2 8 6"></polyline><line x1="12" y1="2" x2="12" y2="15"></line></svg>
|
After Width: | Height: | Size: 364 B |
|
@ -1,3 +1,3 @@
|
||||||
<svg width="12" height="17" viewBox="0 0 12 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M0.857143 14.5C0.857143 15.4491 1.62857 16.5 2.57143 16.5H9.42857C10.3714 16.5 11.1429 15.2542 11.1429 14.3051V5.67692C11.1429 4.72781 10.3714 3.95128 9.42857 3.95128H2.57143C1.62857 3.95128 0.857143 4.72781 0.857143 5.67692V14.5ZM11.1429 1.36282H9L8.39143 0.750218C8.23714 0.59491 8.01429 0.5 7.79143 0.5H4.20857C3.98571 0.5 3.76286 0.59491 3.60857 0.750218L3 1.36282H0.857143C0.385714 1.36282 0 1.75109 0 2.22564C0 2.70019 0.385714 3.08846 0.857143 3.08846H11.1429C11.6143 3.08846 12 2.70019 12 2.22564C12 1.75109 11.6143 1.36282 11.1429 1.36282Z" fill="#737D8C"/>
|
<path d="M6 19C6 20.1 6.9 21 8 21H16C17.1 21 18 20.1 18 19V9C18 7.9 17.1 7 16 7H8C6.9 7 6 7.9 6 9V19ZM18 4H15.5L14.79 3.29C14.61 3.11 14.35 3 14.09 3H9.91C9.65 3 9.39 3.11 9.21 3.29L8.5 4H6C5.45 4 5 4.45 5 5C5 5.55 5.45 6 6 6H18C18.55 6 19 5.55 19 5C19 4.45 18.55 4 18 4Z" fill="#8D99A5"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 397 B |
|
@ -1,5 +1,32 @@
|
||||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<circle cx="8" cy="8" r="8" fill="#FF4B55"/>
|
<svg
|
||||||
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
id="svg8"
|
||||||
|
version="1.1"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
height="24"
|
||||||
|
width="24">
|
||||||
|
<metadata
|
||||||
|
id="metadata14">
|
||||||
|
<rdf:RDF>
|
||||||
|
<cc:Work
|
||||||
|
rdf:about="">
|
||||||
|
<dc:format>image/svg+xml</dc:format>
|
||||||
|
<dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
|
<dc:title></dc:title>
|
||||||
|
</cc:Work>
|
||||||
|
</rdf:RDF>
|
||||||
|
</metadata>
|
||||||
|
<defs
|
||||||
|
id="defs12" />
|
||||||
|
<path
|
||||||
|
id="path2"
|
||||||
|
d="M 12 2 C 6.47715 2 2 6.47715 2 12 C 2 17.5228 6.47715 22 12 22 C 17.5228 22 22 17.5228 22 12 C 22 6.47715 17.5228 2 12 2 z M 11.880859 5.5039062 C 12.720859 5.4439063 13.470547 6.0746875 13.560547 6.9296875 L 13.560547 7.1699219 L 13.080078 13.169922 C 13.035078 13.724922 12.570625 14.144531 12.015625 14.144531 L 11.925781 14.144531 C 11.400781 14.099531 10.996172 13.694922 10.951172 13.169922 L 10.470703 7.1699219 C 10.395703 6.3149219 11.025859 5.5639064 11.880859 5.5039062 z M 12 15.763672 C 12.729 15.763672 13.320312 16.354884 13.320312 17.083984 C 13.320313 17.812984 12.729 18.404297 12 18.404297 C 11.271 18.404297 10.679688 17.812984 10.679688 17.083984 C 10.679688 16.354884 11.271 15.763672 12 15.763672 z "
|
||||||
|
style="fill:#ff4b55;fill-opacity:1" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 283 B After Width: | Height: | Size: 1.5 KiB |
|
@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #6F7882;
|
||||||
|
;
|
||||||
|
|
||||||
|
|
||||||
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $bg-color;
|
$roomlist-filter-active-bg-color: $bg-color;
|
||||||
|
@ -212,8 +215,6 @@ $message-body-panel-icon-fg-color: #21262C; // "Separator"
|
||||||
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
$message-body-panel-icon-bg-color: $tertiary-fg-color;
|
||||||
|
|
||||||
$voice-record-stop-border-color: $quaternary-fg-color;
|
$voice-record-stop-border-color: $quaternary-fg-color;
|
||||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
|
||||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
$voice-record-icon-color: $quaternary-fg-color;
|
$voice-record-icon-color: $quaternary-fg-color;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||||
|
|
|
@ -20,6 +20,9 @@ $tertiary-fg-color: $primary-fg-color;
|
||||||
$primary-bg-color: $bg-color;
|
$primary-bg-color: $bg-color;
|
||||||
$muted-fg-color: $header-panel-text-primary-color;
|
$muted-fg-color: $header-panel-text-primary-color;
|
||||||
|
|
||||||
|
// Legacy theme backports
|
||||||
|
$quaternary-fg-color: #6F7882;
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: $header-panel-text-secondary-color;
|
$light-fg-color: $header-panel-text-secondary-color;
|
||||||
|
|
||||||
|
@ -114,6 +117,8 @@ $voipcall-plinth-color: #394049;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #6F7882;
|
||||||
|
;
|
||||||
|
|
||||||
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
||||||
|
@ -207,8 +212,6 @@ $message-body-panel-icon-bg-color: $secondary-fg-color;
|
||||||
|
|
||||||
// See non-legacy dark for variable information
|
// See non-legacy dark for variable information
|
||||||
$voice-record-stop-border-color: #6F7882;
|
$voice-record-stop-border-color: #6F7882;
|
||||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
|
||||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
|
||||||
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
$voice-record-waveform-incomplete-fg-color: #6F7882;
|
||||||
$voice-record-icon-color: #6F7882;
|
$voice-record-icon-color: #6F7882;
|
||||||
$voice-playback-button-bg-color: $tertiary-fg-color;
|
$voice-playback-button-bg-color: $tertiary-fg-color;
|
||||||
|
|
|
@ -28,6 +28,9 @@ $tertiary-fg-color: $primary-fg-color;
|
||||||
$primary-bg-color: #ffffff;
|
$primary-bg-color: #ffffff;
|
||||||
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
$muted-fg-color: #61708b; // Commonly used in headings and relevant alt text
|
||||||
|
|
||||||
|
// Legacy theme backports
|
||||||
|
$quaternary-fg-color: #C1C6CD;
|
||||||
|
|
||||||
// used for dialog box text
|
// used for dialog box text
|
||||||
$light-fg-color: #747474;
|
$light-fg-color: #747474;
|
||||||
|
|
||||||
|
@ -181,6 +184,8 @@ $voipcall-plinth-color: #F4F6FA;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #e3e8f0;
|
||||||
|
|
||||||
|
|
||||||
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
$roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
||||||
|
@ -332,8 +337,6 @@ $message-body-panel-icon-bg-color: $primary-bg-color;
|
||||||
$voice-record-stop-symbol-color: #ff4b55;
|
$voice-record-stop-symbol-color: #ff4b55;
|
||||||
$voice-record-live-circle-color: #ff4b55;
|
$voice-record-live-circle-color: #ff4b55;
|
||||||
$voice-record-stop-border-color: #E3E8F0;
|
$voice-record-stop-border-color: #E3E8F0;
|
||||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
|
||||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
|
||||||
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
$voice-record-waveform-incomplete-fg-color: #C1C6CD;
|
||||||
$voice-record-icon-color: $tertiary-fg-color;
|
$voice-record-icon-color: $tertiary-fg-color;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||||
|
|
|
@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$theme-button-bg-color: #e3e8f0;
|
$theme-button-bg-color: #e3e8f0;
|
||||||
|
$dialpad-button-bg-color: #e3e8f0;
|
||||||
|
|
||||||
|
|
||||||
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
|
||||||
$roomlist-filter-active-bg-color: #ffffff;
|
$roomlist-filter-active-bg-color: #ffffff;
|
||||||
|
@ -333,8 +335,6 @@ $voice-record-stop-symbol-color: #ff4b55;
|
||||||
$voice-record-live-circle-color: #ff4b55;
|
$voice-record-live-circle-color: #ff4b55;
|
||||||
|
|
||||||
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
$voice-record-stop-border-color: #E3E8F0; // "Separator"
|
||||||
$voice-record-waveform-bg-color: $message-body-panel-bg-color;
|
|
||||||
$voice-record-waveform-fg-color: $message-body-panel-fg-color;
|
|
||||||
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
|
||||||
$voice-record-icon-color: $tertiary-fg-color;
|
$voice-record-icon-color: $tertiary-fg-color;
|
||||||
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
$voice-playback-button-bg-color: $message-body-panel-icon-bg-color;
|
||||||
|
|
|
@ -5,4 +5,4 @@ FROM node:14-buster
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
|
RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
|
||||||
# dependencies for chrome (installed by puppeteer)
|
# dependencies for chrome (installed by puppeteer)
|
||||||
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm-dev libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
|
||||||
# script which is run by the CI build (after `yarn test`).
|
|
||||||
#
|
|
||||||
# clones element-web develop and runs the tests against our version of react-sdk.
|
|
||||||
|
|
||||||
set -ev
|
set -ev
|
||||||
|
|
||||||
|
@ -19,7 +15,7 @@ cd element-web
|
||||||
element_web_dir=`pwd`
|
element_web_dir=`pwd`
|
||||||
CI_PACKAGE=true yarn build
|
CI_PACKAGE=true yarn build
|
||||||
cd ..
|
cd ..
|
||||||
# run end to end tests
|
# prepare end to end tests
|
||||||
pushd test/end-to-end-tests
|
pushd test/end-to-end-tests
|
||||||
ln -s $element_web_dir element/element-web
|
ln -s $element_web_dir element/element-web
|
||||||
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
||||||
|
@ -28,9 +24,4 @@ echo "--- Install synapse & other dependencies"
|
||||||
./install.sh
|
./install.sh
|
||||||
# install static webserver to server symlinked local copy of element
|
# install static webserver to server symlinked local copy of element
|
||||||
./element/install-webserver.sh
|
./element/install-webserver.sh
|
||||||
rm -r logs || true
|
|
||||||
mkdir logs
|
|
||||||
echo "+++ Running end-to-end tests"
|
|
||||||
TESTS_STARTED=1
|
|
||||||
./run.sh --no-sandbox --log-directory logs/
|
|
||||||
popd
|
popd
|
19
scripts/ci/run-end-to-end-tests.sh
Executable file
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -ev
|
||||||
|
|
||||||
|
handle_error() {
|
||||||
|
EXIT_CODE=$?
|
||||||
|
exit $EXIT_CODE
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'handle_error' ERR
|
||||||
|
|
||||||
|
# run end to end tests
|
||||||
|
pushd test/end-to-end-tests
|
||||||
|
rm -r logs || true
|
||||||
|
mkdir logs
|
||||||
|
echo "--- Running end-to-end tests"
|
||||||
|
TESTS_STARTED=1
|
||||||
|
./run.sh --no-sandbox --log-directory logs/
|
||||||
|
popd
|
|
@ -22,29 +22,51 @@ clone() {
|
||||||
}
|
}
|
||||||
|
|
||||||
# Try the PR author's branch in case it exists on the deps as well.
|
# Try the PR author's branch in case it exists on the deps as well.
|
||||||
# First we check if BUILDKITE_BRANCH is defined,
|
# First we check if GITHUB_HEAD_REF is defined,
|
||||||
# if it isn't we can assume this is a Netlify build
|
# Then we check if BUILDKITE_BRANCH is defined,
|
||||||
if [ -z ${BUILDKITE_BRANCH+x} ]; then
|
# if they aren't we can assume this is a Netlify build
|
||||||
# Netlify doesn't give us info about the fork so we have to get it from GitHub API
|
if [ -n "$GITHUB_HEAD_REF" ]; then
|
||||||
apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/"
|
head=$GITHUB_HEAD_REF
|
||||||
apiEndpoint+=$REVIEW_ID
|
elif [ -n "$BUILDKITE_BRANCH" ]; then
|
||||||
head=$(curl $apiEndpoint | jq -r '.head.label')
|
|
||||||
else
|
|
||||||
head=$BUILDKITE_BRANCH
|
head=$BUILDKITE_BRANCH
|
||||||
|
else
|
||||||
|
# Netlify doesn't give us info about the fork so we have to get it from GitHub API
|
||||||
|
apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/"
|
||||||
|
apiEndpoint+=$REVIEW_ID
|
||||||
|
head=$(curl $apiEndpoint | jq -r '.head.label')
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If head is set, it will contain either:
|
# If head is set, it will contain on Buildkite either:
|
||||||
# * "branch" when the author's branch and target branch are in the same repo
|
# * "branch" when the author's branch and target branch are in the same repo
|
||||||
# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build
|
# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build
|
||||||
# We can split on `:` into an array to check.
|
# We can split on `:` into an array to check.
|
||||||
|
# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR
|
||||||
|
# to determine whether the branch is from a fork or not
|
||||||
BRANCH_ARRAY=(${head//:/ })
|
BRANCH_ARRAY=(${head//:/ })
|
||||||
if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then
|
if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then
|
||||||
clone $deforg $defrepo $BUILDKITE_BRANCH
|
|
||||||
|
if [ -n "$GITHUB_HEAD_REF" ]; then
|
||||||
|
if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then
|
||||||
|
clone $deforg $defrepo $GITHUB_HEAD_REF
|
||||||
|
else
|
||||||
|
REPO_ARRAY=(${GITHUB_REPOSITORY//\// })
|
||||||
|
clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
clone $deforg $defrepo $BUILDKITE_BRANCH
|
||||||
|
fi
|
||||||
|
|
||||||
elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then
|
elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then
|
||||||
clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]}
|
clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]}
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Try the target branch of the push or PR.
|
# Try the target branch of the push or PR.
|
||||||
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
if [ -n $GITHUB_BASE_REF ]; then
|
||||||
|
clone $deforg $defrepo $GITHUB_BASE_REF
|
||||||
|
elif [ -n $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]; then
|
||||||
|
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
|
||||||
|
fi
|
||||||
|
|
||||||
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
|
||||||
clone $deforg $defrepo $HEAD
|
clone $deforg $defrepo $HEAD
|
||||||
# Use the default branch as the last resort.
|
# Use the default branch as the last resort.
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
# generates .eslintignore.errorfiles to list the files which have errors in,
|
|
||||||
# so that they can be ignored in future automated linting.
|
|
||||||
|
|
||||||
out=.eslintignore.errorfiles
|
|
||||||
|
|
||||||
cd `dirname $0`/..
|
|
||||||
|
|
||||||
echo "generating $out"
|
|
||||||
|
|
||||||
{
|
|
||||||
cat <<EOF
|
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
|
||||||
|
|
||||||
EOF
|
|
||||||
|
|
||||||
./node_modules/.bin/eslint -f json src test |
|
|
||||||
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
|
|
||||||
sed -e 's/.*matrix-react-sdk\///';
|
|
||||||
} > "$out"
|
|
||||||
# also append rules from eslintignore file
|
|
||||||
cat .eslintignore >> $out
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import { JSXElementConstructor } from "react";
|
import { JSXElementConstructor } from "react";
|
||||||
|
|
||||||
// Based on https://stackoverflow.com/a/53229857/3532235
|
// Based on https://stackoverflow.com/a/53229857/3532235
|
||||||
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>] ? : never};
|
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
|
||||||
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;
|
||||||
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
|
||||||
|
|
||||||
|
|
38
src/@types/diff-dom.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare module "diff-dom" {
|
||||||
|
export interface IDiff {
|
||||||
|
action: string;
|
||||||
|
name: string;
|
||||||
|
text?: string;
|
||||||
|
route: number[];
|
||||||
|
value: string;
|
||||||
|
element: unknown;
|
||||||
|
oldValue: string;
|
||||||
|
newValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IOpts {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DiffDOM {
|
||||||
|
public constructor(opts?: IOpts);
|
||||||
|
public apply(tree: unknown, diffs: IDiff[]): unknown;
|
||||||
|
public undo(tree: unknown, diffs: IDiff[]): unknown;
|
||||||
|
public diff(a: HTMLElement | string, b: HTMLElement | string): IDiff[];
|
||||||
|
}
|
||||||
|
}
|
45
src/@types/global.d.ts
vendored
|
@ -16,6 +16,7 @@ limitations under the License.
|
||||||
|
|
||||||
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
||||||
import * as ModernizrStatic from "modernizr";
|
import * as ModernizrStatic from "modernizr";
|
||||||
|
|
||||||
import ContentMessages from "../ContentMessages";
|
import ContentMessages from "../ContentMessages";
|
||||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
|
@ -23,25 +24,25 @@ import DeviceListener from "../DeviceListener";
|
||||||
import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
|
import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
|
||||||
import { PlatformPeg } from "../PlatformPeg";
|
import { PlatformPeg } from "../PlatformPeg";
|
||||||
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
|
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
|
||||||
import {IntegrationManagers} from "../integrations/IntegrationManagers";
|
import { IntegrationManagers } from "../integrations/IntegrationManagers";
|
||||||
import {ModalManager} from "../Modal";
|
import { ModalManager } from "../Modal";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import {ActiveRoomObserver} from "../ActiveRoomObserver";
|
import { ActiveRoomObserver } from "../ActiveRoomObserver";
|
||||||
import {Notifier} from "../Notifier";
|
import { Notifier } from "../Notifier";
|
||||||
import type {Renderer} from "react-dom";
|
import type { Renderer } from "react-dom";
|
||||||
import RightPanelStore from "../stores/RightPanelStore";
|
import RightPanelStore from "../stores/RightPanelStore";
|
||||||
import WidgetStore from "../stores/WidgetStore";
|
import WidgetStore from "../stores/WidgetStore";
|
||||||
import CallHandler from "../CallHandler";
|
import CallHandler from "../CallHandler";
|
||||||
import {Analytics} from "../Analytics";
|
import { Analytics } from "../Analytics";
|
||||||
import CountlyAnalytics from "../CountlyAnalytics";
|
import CountlyAnalytics from "../CountlyAnalytics";
|
||||||
import UserActivity from "../UserActivity";
|
import UserActivity from "../UserActivity";
|
||||||
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
import { ModalWidgetStore } from "../stores/ModalWidgetStore";
|
||||||
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
|
||||||
import VoipUserMapper from "../VoipUserMapper";
|
import VoipUserMapper from "../VoipUserMapper";
|
||||||
import {SpaceStoreClass} from "../stores/SpaceStore";
|
import { SpaceStoreClass } from "../stores/SpaceStore";
|
||||||
import TypingStore from "../stores/TypingStore";
|
import TypingStore from "../stores/TypingStore";
|
||||||
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
import { EventIndexPeg } from "../indexing/EventIndexPeg";
|
||||||
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
|
import { VoiceRecordingStore } from "../stores/VoiceRecordingStore";
|
||||||
import PerformanceMonitor from "../performance";
|
import PerformanceMonitor from "../performance";
|
||||||
import UIStore from "../stores/UIStore";
|
import UIStore from "../stores/UIStore";
|
||||||
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
|
||||||
|
@ -113,19 +114,6 @@ declare global {
|
||||||
usageDetails?: {[key: string]: number};
|
usageDetails?: {[key: string]: number};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISettledFulfilled<T> {
|
|
||||||
status: "fulfilled";
|
|
||||||
value: T;
|
|
||||||
}
|
|
||||||
export interface ISettledRejected {
|
|
||||||
status: "rejected";
|
|
||||||
reason: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PromiseConstructor {
|
|
||||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
type?: string;
|
type?: string;
|
||||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||||
|
@ -140,11 +128,24 @@ declare global {
|
||||||
setSinkId(outputId: string);
|
setSinkId(outputId: string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add Chrome-specific `instant` ScrollBehaviour
|
||||||
|
type _ScrollBehavior = ScrollBehavior | "instant";
|
||||||
|
|
||||||
|
interface _ScrollOptions {
|
||||||
|
behavior?: _ScrollBehavior;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface _ScrollIntoViewOptions extends _ScrollOptions {
|
||||||
|
block?: ScrollLogicalPosition;
|
||||||
|
inline?: ScrollLogicalPosition;
|
||||||
|
}
|
||||||
|
|
||||||
interface Element {
|
interface Element {
|
||||||
// Safari & IE11 only have this prefixed: we used prefixed versions
|
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||||
// previously so let's continue to support them for now
|
// previously so let's continue to support them for now
|
||||||
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
||||||
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
|
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Error {
|
interface Error {
|
||||||
|
|
|
@ -16,12 +16,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import IdentityAuthClient from './IdentityAuthClient';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
import {SSOAuthEntry} from "./components/views/auth/InteractiveAuthEntryComponents";
|
import { SSOAuthEntry } from "./components/views/auth/InteractiveAuthEntryComponents";
|
||||||
|
|
||||||
function getIdServerDomain() {
|
function getIdServerDomain() {
|
||||||
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
return MatrixClientPeg.get().idBaseUrl.split("://")[1];
|
||||||
|
@ -189,7 +189,6 @@ export default class AddThreepid {
|
||||||
// pop up an interactive auth dialog
|
// pop up an interactive auth dialog
|
||||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||||
|
|
||||||
|
|
||||||
const dialogAesthetics = {
|
const dialogAesthetics = {
|
||||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||||
title: _t("Use Single Sign On to continue"),
|
title: _t("Use Single Sign On to continue"),
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler';
|
import { getCurrentLanguage, _t, _td, IVariables } from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
|
@ -17,13 +17,12 @@ limitations under the License.
|
||||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import { User } from "matrix-js-sdk/src/models/user";
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
import DMRoomMap from './utils/DMRoomMap';
|
||||||
import { mediaFromMxc } from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
export type ResizeMethod = "crop" | "scale";
|
|
||||||
|
|
||||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||||
export function avatarUrlForMember(
|
export function avatarUrlForMember(
|
||||||
member: RoomMember,
|
member: RoomMember,
|
||||||
|
|
|
@ -17,16 +17,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {encodeUnpaddedBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
import { encodeUnpaddedBase64 } from "matrix-js-sdk/src/crypto/olmlib";
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
||||||
import {ActionPayload} from "./dispatcher/payloads";
|
import { ActionPayload } from "./dispatcher/payloads";
|
||||||
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
import { CheckUpdatesPayload } from "./dispatcher/payloads/CheckUpdatesPayload";
|
||||||
import {Action} from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
import { hideToast as hideUpdateToast } from "./toasts/UpdateToast";
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
|
import { idbLoad, idbSave, idbDelete } from "./utils/StorageManager";
|
||||||
|
|
||||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||||
|
@ -335,7 +335,7 @@ export default abstract class BasePlatform {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const key = await crypto.subtle.decrypt(
|
const key = await crypto.subtle.decrypt(
|
||||||
{name: "AES-GCM", iv: data.iv, additionalData}, data.cryptoKey,
|
{ name: "AES-GCM", iv: data.iv, additionalData }, data.cryptoKey,
|
||||||
data.encrypted,
|
data.encrypted,
|
||||||
);
|
);
|
||||||
return encodeUnpaddedBase64(key);
|
return encodeUnpaddedBase64(key);
|
||||||
|
@ -348,7 +348,7 @@ export default abstract class BasePlatform {
|
||||||
/**
|
/**
|
||||||
* Create and store a pickle key for encrypting libolm objects.
|
* Create and store a pickle key for encrypting libolm objects.
|
||||||
* @param {string} userId the user ID for the user that the pickle key is for.
|
* @param {string} userId the user ID for the user that the pickle key is for.
|
||||||
* @param {string} userId the device ID that the pickle key is for.
|
* @param {string} deviceId the device ID that the pickle key is for.
|
||||||
* @returns {string|null} the pickle key, or null if the platform does not
|
* @returns {string|null} the pickle key, or null if the platform does not
|
||||||
* support storing pickle keys.
|
* support storing pickle keys.
|
||||||
*/
|
*/
|
||||||
|
@ -360,7 +360,7 @@ export default abstract class BasePlatform {
|
||||||
const randomArray = new Uint8Array(32);
|
const randomArray = new Uint8Array(32);
|
||||||
crypto.getRandomValues(randomArray);
|
crypto.getRandomValues(randomArray);
|
||||||
const cryptoKey = await crypto.subtle.generateKey(
|
const cryptoKey = await crypto.subtle.generateKey(
|
||||||
{name: "AES-GCM", length: 256}, false, ["encrypt", "decrypt"],
|
{ name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"],
|
||||||
);
|
);
|
||||||
const iv = new Uint8Array(32);
|
const iv = new Uint8Array(32);
|
||||||
crypto.getRandomValues(iv);
|
crypto.getRandomValues(iv);
|
||||||
|
@ -375,11 +375,11 @@ export default abstract class BasePlatform {
|
||||||
}
|
}
|
||||||
|
|
||||||
const encrypted = await crypto.subtle.encrypt(
|
const encrypted = await crypto.subtle.encrypt(
|
||||||
{name: "AES-GCM", iv, additionalData}, cryptoKey, randomArray,
|
{ name: "AES-GCM", iv, additionalData }, cryptoKey, randomArray,
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await idbSave("pickleKey", [userId, deviceId], {encrypted, iv, cryptoKey});
|
await idbSave("pickleKey", [userId, deviceId], { encrypted, iv, cryptoKey });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
@ -63,11 +63,11 @@ import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import {Jitsi} from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import {WidgetType} from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import { ActionPayload } from "./dispatcher/payloads";
|
import { ActionPayload } from "./dispatcher/payloads";
|
||||||
import {base32} from "rfc4648";
|
import { base32 } from "rfc4648";
|
||||||
|
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
|
@ -77,10 +77,10 @@ import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
|
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import { UIFeature } from "./settings/UIFeature";
|
||||||
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
import { CallError } from "matrix-js-sdk/src/webrtc/call";
|
||||||
import { logger } from 'matrix-js-sdk/src/logger';
|
import { logger } from 'matrix-js-sdk/src/logger';
|
||||||
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
|
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker";
|
||||||
import { Action } from './dispatcher/actions';
|
import { Action } from './dispatcher/actions';
|
||||||
import VoipUserMapper from './VoipUserMapper';
|
import VoipUserMapper from './VoipUserMapper';
|
||||||
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
|
||||||
|
@ -124,9 +124,9 @@ interface ThirdpartyLookupResponseFields {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ThirdpartyLookupResponse {
|
interface ThirdpartyLookupResponse {
|
||||||
userid: string,
|
userid: string;
|
||||||
protocol: string,
|
protocol: string;
|
||||||
fields: ThirdpartyLookupResponseFields,
|
fields: ThirdpartyLookupResponseFields;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
||||||
|
@ -166,7 +166,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (!window.mxCallHandler) {
|
if (!window.mxCallHandler) {
|
||||||
window.mxCallHandler = new CallHandler()
|
window.mxCallHandler = new CallHandler();
|
||||||
}
|
}
|
||||||
|
|
||||||
return window.mxCallHandler;
|
return window.mxCallHandler;
|
||||||
|
@ -185,7 +185,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
const nativeUser = this.assertedIdentityNativeUsers[call.callId];
|
const nativeUser = this.assertedIdentityNativeUsers[call.callId];
|
||||||
if (nativeUser) {
|
if (nativeUser) {
|
||||||
const room = findDMForUser(MatrixClientPeg.get(), nativeUser);
|
const room = findDMForUser(MatrixClientPeg.get(), nativeUser);
|
||||||
if (room) return room.roomId
|
if (room) return room.roomId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +238,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
this.supportsPstnProtocol = null;
|
this.supportsPstnProtocol = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
dis.dispatch({action: Action.PstnSupportUpdated});
|
dis.dispatch({ action: Action.PstnSupportUpdated });
|
||||||
|
|
||||||
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
|
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
|
||||||
this.supportsSipNativeVirtual = Boolean(
|
this.supportsSipNativeVirtual = Boolean(
|
||||||
|
@ -246,7 +246,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dis.dispatch({action: Action.VirtualRoomSupportUpdated});
|
dis.dispatch({ action: Action.VirtualRoomSupportUpdated });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (maxTries === 1) {
|
if (maxTries === 1) {
|
||||||
console.log("Failed to check for protocol support and no retries remain: assuming no support", e);
|
console.log("Failed to check for protocol support and no retries remain: assuming no support", e);
|
||||||
|
@ -299,7 +299,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
action: 'incoming_call',
|
action: 'incoming_call',
|
||||||
call: call,
|
call: call,
|
||||||
}, true);
|
}, true);
|
||||||
}
|
};
|
||||||
|
|
||||||
getCallForRoom(roomId: string): MatrixCall {
|
getCallForRoom(roomId: string): MatrixCall {
|
||||||
return this.calls.get(roomId) || null;
|
return this.calls.get(roomId) || null;
|
||||||
|
@ -711,7 +711,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
call.placeScreenSharingCall(
|
call.placeScreenSharingCall(
|
||||||
async (): Promise<DesktopCapturerSource> => {
|
async (): Promise<DesktopCapturerSource> => {
|
||||||
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
|
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
|
||||||
const [source] = await finished;
|
const [source] = await finished;
|
||||||
return source;
|
return source;
|
||||||
},
|
},
|
||||||
|
@ -816,7 +816,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
|
|
||||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
console.log("Adding call for room ", mappedRoomId);
|
console.log("Adding call for room ", mappedRoomId);
|
||||||
this.calls.set(mappedRoomId, call)
|
this.calls.set(mappedRoomId, call);
|
||||||
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
this.emit(CallHandlerEvent.CallsChanged, this.calls);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
|
||||||
|
@ -872,7 +872,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
this.dialNumber(payload.number);
|
this.dialNumber(payload.number);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private async dialNumber(number: string) {
|
private async dialNumber(number: string) {
|
||||||
const results = await this.pstnLookup(number);
|
const results = await this.pstnLookup(number);
|
||||||
|
@ -966,7 +966,7 @@ export default class CallHandler extends EventEmitter {
|
||||||
confId = 'Jitsi' + random;
|
confId = 'Jitsi' + random;
|
||||||
}
|
}
|
||||||
|
|
||||||
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
|
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({ auth: jitsiAuth });
|
||||||
|
|
||||||
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
// TODO: Remove URL hacks when the mobile clients eventually support v2 widgets
|
||||||
const parsedUrl = new URL(widgetUrl);
|
const parsedUrl = new URL(widgetUrl);
|
||||||
|
|
|
@ -1,85 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
|
||||||
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
|
|
||||||
|
|
||||||
export default {
|
|
||||||
hasAnyLabeledDevices: async function() {
|
|
||||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
||||||
return devices.some(d => !!d.label);
|
|
||||||
},
|
|
||||||
|
|
||||||
getDevices: function() {
|
|
||||||
// Only needed for Electron atm, though should work in modern browsers
|
|
||||||
// once permission has been granted to the webapp
|
|
||||||
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
|
||||||
const audiooutput = [];
|
|
||||||
const audioinput = [];
|
|
||||||
const videoinput = [];
|
|
||||||
|
|
||||||
devices.forEach((device) => {
|
|
||||||
switch (device.kind) {
|
|
||||||
case 'audiooutput': audiooutput.push(device); break;
|
|
||||||
case 'audioinput': audioinput.push(device); break;
|
|
||||||
case 'videoinput': videoinput.push(device); break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// console.log("Loaded WebRTC Devices", mediaDevices);
|
|
||||||
return {
|
|
||||||
audiooutput,
|
|
||||||
audioinput,
|
|
||||||
videoinput,
|
|
||||||
};
|
|
||||||
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
|
|
||||||
},
|
|
||||||
|
|
||||||
loadDevices: function() {
|
|
||||||
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
|
||||||
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
|
||||||
|
|
||||||
setMatrixCallAudioInput(audioDeviceId);
|
|
||||||
setMatrixCallVideoInput(videoDeviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setAudioOutput: function(deviceId) {
|
|
||||||
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setAudioInput: function(deviceId) {
|
|
||||||
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
|
||||||
setMatrixCallAudioInput(deviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
setVideoInput: function(deviceId) {
|
|
||||||
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
|
||||||
setMatrixCallVideoInput(deviceId);
|
|
||||||
},
|
|
||||||
|
|
||||||
getAudioOutput: function() {
|
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
|
||||||
},
|
|
||||||
|
|
||||||
getAudioInput: function() {
|
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
|
||||||
},
|
|
||||||
|
|
||||||
getVideoInput: function() {
|
|
||||||
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -17,9 +17,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { encode } from "blurhash";
|
||||||
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
@ -37,7 +38,7 @@ import {
|
||||||
UploadProgressPayload,
|
UploadProgressPayload,
|
||||||
UploadStartedPayload,
|
UploadStartedPayload,
|
||||||
} from "./dispatcher/payloads/UploadPayload";
|
} from "./dispatcher/payloads/UploadPayload";
|
||||||
import {IUpload} from "./models/IUpload";
|
import { IUpload } from "./models/IUpload";
|
||||||
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
import { IImageInfo } from "matrix-js-sdk/src/@types/partials";
|
||||||
|
|
||||||
const MAX_WIDTH = 800;
|
const MAX_WIDTH = 800;
|
||||||
|
@ -47,6 +48,10 @@ const MAX_HEIGHT = 600;
|
||||||
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
// 5669 px (x-axis) , 5669 px (y-axis) , per metre
|
||||||
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
const PHYS_HIDPI = [0x00, 0x00, 0x16, 0x25, 0x00, 0x00, 0x16, 0x25, 0x01];
|
||||||
|
|
||||||
|
export const BLURHASH_FIELD = "xyz.amorgan.blurhash"; // MSC2448
|
||||||
|
const BLURHASH_X_COMPONENTS = 6;
|
||||||
|
const BLURHASH_Y_COMPONENTS = 6;
|
||||||
|
|
||||||
export class UploadCanceledError extends Error {}
|
export class UploadCanceledError extends Error {}
|
||||||
|
|
||||||
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
|
||||||
|
@ -77,6 +82,7 @@ interface IThumbnail {
|
||||||
};
|
};
|
||||||
w: number;
|
w: number;
|
||||||
h: number;
|
h: number;
|
||||||
|
[BLURHASH_FIELD]: string;
|
||||||
};
|
};
|
||||||
thumbnail: Blob;
|
thumbnail: Blob;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +130,16 @@ function createThumbnail(
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = targetWidth;
|
canvas.width = targetWidth;
|
||||||
canvas.height = targetHeight;
|
canvas.height = targetHeight;
|
||||||
canvas.getContext("2d").drawImage(element, 0, 0, targetWidth, targetHeight);
|
const context = canvas.getContext("2d");
|
||||||
|
context.drawImage(element, 0, 0, targetWidth, targetHeight);
|
||||||
|
const imageData = context.getImageData(0, 0, targetWidth, targetHeight);
|
||||||
|
const blurhash = encode(
|
||||||
|
imageData.data,
|
||||||
|
imageData.width,
|
||||||
|
imageData.height,
|
||||||
|
BLURHASH_X_COMPONENTS,
|
||||||
|
BLURHASH_Y_COMPONENTS,
|
||||||
|
);
|
||||||
canvas.toBlob(function(thumbnail) {
|
canvas.toBlob(function(thumbnail) {
|
||||||
resolve({
|
resolve({
|
||||||
info: {
|
info: {
|
||||||
|
@ -136,8 +151,9 @@ function createThumbnail(
|
||||||
},
|
},
|
||||||
w: inputWidth,
|
w: inputWidth,
|
||||||
h: inputHeight,
|
h: inputHeight,
|
||||||
|
[BLURHASH_FIELD]: blurhash,
|
||||||
},
|
},
|
||||||
thumbnail: thumbnail,
|
thumbnail,
|
||||||
});
|
});
|
||||||
}, mimeType);
|
}, mimeType);
|
||||||
});
|
});
|
||||||
|
@ -189,7 +205,7 @@ async function loadImageElement(imageFile: File) {
|
||||||
const [hidpi] = await Promise.all([parsePromise, imgPromise]);
|
const [hidpi] = await Promise.all([parsePromise, imgPromise]);
|
||||||
const width = hidpi ? (img.width >> 1) : img.width;
|
const width = hidpi ? (img.width >> 1) : img.width;
|
||||||
const height = hidpi ? (img.height >> 1) : img.height;
|
const height = hidpi ? (img.height >> 1) : img.height;
|
||||||
return {width, height, img};
|
return { width, height, img };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -220,7 +236,8 @@ function infoForImageFile(matrixClient, roomId, imageFile) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a file into a newly created video element.
|
* Load a file into a newly created video element and pull some strings
|
||||||
|
* in an attempt to guarantee the first frame will be showing.
|
||||||
*
|
*
|
||||||
* @param {File} videoFile The file to load in an video element.
|
* @param {File} videoFile The file to load in an video element.
|
||||||
* @return {Promise} A promise that resolves with the video image element.
|
* @return {Promise} A promise that resolves with the video image element.
|
||||||
|
@ -229,20 +246,25 @@ function loadVideoElement(videoFile): Promise<HTMLVideoElement> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Load the file into an html element
|
// Load the file into an html element
|
||||||
const video = document.createElement("video");
|
const video = document.createElement("video");
|
||||||
|
video.preload = "metadata";
|
||||||
|
video.playsInline = true;
|
||||||
|
video.muted = true;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function(ev) {
|
reader.onload = function(ev) {
|
||||||
video.src = ev.target.result as string;
|
|
||||||
|
|
||||||
// Once ready, returns its size
|
|
||||||
// Wait until we have enough data to thumbnail the first frame.
|
// Wait until we have enough data to thumbnail the first frame.
|
||||||
video.onloadeddata = function() {
|
video.onloadeddata = async function() {
|
||||||
resolve(video);
|
resolve(video);
|
||||||
|
video.pause();
|
||||||
};
|
};
|
||||||
video.onerror = function(e) {
|
video.onerror = function(e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
video.src = ev.target.result as string;
|
||||||
|
video.load();
|
||||||
|
video.play();
|
||||||
};
|
};
|
||||||
reader.onerror = function(e) {
|
reader.onerror = function(e) {
|
||||||
reject(e);
|
reject(e);
|
||||||
|
@ -307,7 +329,7 @@ function readFileAsArrayBuffer(file: File | Blob): Promise<ArrayBuffer> {
|
||||||
* If the file is unencrypted then the object will have a "url" key.
|
* If the file is unencrypted then the object will have a "url" key.
|
||||||
* If the file is encrypted then the object will have a "file" key.
|
* If the file is encrypted then the object will have a "file" key.
|
||||||
*/
|
*/
|
||||||
function uploadFile(
|
export function uploadFile(
|
||||||
matrixClient: MatrixClient,
|
matrixClient: MatrixClient,
|
||||||
roomId: string,
|
roomId: string,
|
||||||
file: File | Blob,
|
file: File | Blob,
|
||||||
|
@ -343,11 +365,11 @@ function uploadFile(
|
||||||
if (file.type) {
|
if (file.type) {
|
||||||
encryptInfo.mimetype = file.type;
|
encryptInfo.mimetype = file.type;
|
||||||
}
|
}
|
||||||
return {"file": encryptInfo};
|
return { "file": encryptInfo };
|
||||||
});
|
});
|
||||||
(prom as IAbortablePromise<any>).abort = () => {
|
(prom as IAbortablePromise<any>).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
if (uploadPromise) MatrixClientPeg.get().cancelUpload(uploadPromise);
|
if (uploadPromise) matrixClient.cancelUpload(uploadPromise);
|
||||||
};
|
};
|
||||||
return prom;
|
return prom;
|
||||||
} else {
|
} else {
|
||||||
|
@ -357,11 +379,11 @@ function uploadFile(
|
||||||
const promise1 = basePromise.then(function(url) {
|
const promise1 = basePromise.then(function(url) {
|
||||||
if (canceled) throw new UploadCanceledError();
|
if (canceled) throw new UploadCanceledError();
|
||||||
// If the attachment isn't encrypted then include the URL directly.
|
// If the attachment isn't encrypted then include the URL directly.
|
||||||
return {"url": url};
|
return { url };
|
||||||
});
|
});
|
||||||
(promise1 as any).abort = () => {
|
(promise1 as any).abort = () => {
|
||||||
canceled = true;
|
canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(basePromise);
|
matrixClient.cancelUpload(basePromise);
|
||||||
};
|
};
|
||||||
return promise1;
|
return promise1;
|
||||||
}
|
}
|
||||||
|
@ -373,11 +395,11 @@ export default class ContentMessages {
|
||||||
|
|
||||||
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
sendStickerContentToRoom(url: string, roomId: string, info: IImageInfo, text: string, matrixClient: MatrixClient) {
|
||||||
const startTime = CountlyAnalytics.getTimestamp();
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
const prom = matrixClient.sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, {msgtype: "m.sticker"});
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, { msgtype: "m.sticker" });
|
||||||
return prom;
|
return prom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -391,14 +413,14 @@ export default class ContentMessages {
|
||||||
|
|
||||||
async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) {
|
async sendContentListToRoom(files: File[], roomId: string, matrixClient: MatrixClient) {
|
||||||
if (matrixClient.isGuest()) {
|
if (matrixClient.isGuest()) {
|
||||||
dis.dispatch({action: 'require_registration'});
|
dis.dispatch({ action: 'require_registration' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
||||||
if (isQuoting) {
|
if (isQuoting) {
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
|
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
|
||||||
title: _t('Replying With Files'),
|
title: _t('Replying With Files'),
|
||||||
description: (
|
description: (
|
||||||
<div>{_t(
|
<div>{_t(
|
||||||
|
@ -415,7 +437,7 @@ export default class ContentMessages {
|
||||||
|
|
||||||
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
if (!this.mediaConfig) { // hot-path optimization to not flash a spinner if we don't need to
|
||||||
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
const modal = Modal.createDialog(Spinner, null, 'mx_Dialog_spinner');
|
||||||
await this.ensureMediaConfigFetched();
|
await this.ensureMediaConfigFetched(matrixClient);
|
||||||
modal.close();
|
modal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,7 +454,7 @@ export default class ContentMessages {
|
||||||
|
|
||||||
if (tooBigFiles.length > 0) {
|
if (tooBigFiles.length > 0) {
|
||||||
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
|
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
|
||||||
const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
|
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
|
||||||
badFiles: tooBigFiles,
|
badFiles: tooBigFiles,
|
||||||
totalFiles: files.length,
|
totalFiles: files.length,
|
||||||
contentMessages: this,
|
contentMessages: this,
|
||||||
|
@ -449,7 +471,7 @@ export default class ContentMessages {
|
||||||
for (let i = 0; i < okFiles.length; ++i) {
|
for (let i = 0; i < okFiles.length; ++i) {
|
||||||
const file = okFiles[i];
|
const file = okFiles[i];
|
||||||
if (!uploadAll) {
|
if (!uploadAll) {
|
||||||
const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
|
const { finished } = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
|
||||||
'', UploadConfirmDialog, {
|
'', UploadConfirmDialog, {
|
||||||
file,
|
file,
|
||||||
currentIndex: i,
|
currentIndex: i,
|
||||||
|
@ -470,7 +492,7 @@ export default class ContentMessages {
|
||||||
return this.inprogress.filter(u => !u.canceled);
|
return this.inprogress.filter(u => !u.canceled);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancelUpload(promise: Promise<any>) {
|
cancelUpload(promise: Promise<any>, matrixClient: MatrixClient) {
|
||||||
let upload: IUpload;
|
let upload: IUpload;
|
||||||
for (let i = 0; i < this.inprogress.length; ++i) {
|
for (let i = 0; i < this.inprogress.length; ++i) {
|
||||||
if (this.inprogress[i].promise === promise) {
|
if (this.inprogress[i].promise === promise) {
|
||||||
|
@ -480,8 +502,8 @@ export default class ContentMessages {
|
||||||
}
|
}
|
||||||
if (upload) {
|
if (upload) {
|
||||||
upload.canceled = true;
|
upload.canceled = true;
|
||||||
MatrixClientPeg.get().cancelUpload(upload.promise);
|
matrixClient.cancelUpload(upload.promise);
|
||||||
dis.dispatch<UploadCanceledPayload>({action: Action.UploadCanceled, upload});
|
dis.dispatch<UploadCanceledPayload>({ action: Action.UploadCanceled, upload });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -542,7 +564,7 @@ export default class ContentMessages {
|
||||||
promise: prom,
|
promise: prom,
|
||||||
};
|
};
|
||||||
this.inprogress.push(upload);
|
this.inprogress.push(upload);
|
||||||
dis.dispatch<UploadStartedPayload>({action: Action.UploadStarted, upload});
|
dis.dispatch<UploadStartedPayload>({ action: Action.UploadStarted, upload });
|
||||||
|
|
||||||
// Focus the composer view
|
// Focus the composer view
|
||||||
dis.fire(Action.FocusComposer);
|
dis.fire(Action.FocusComposer);
|
||||||
|
@ -550,7 +572,7 @@ export default class ContentMessages {
|
||||||
function onProgress(ev) {
|
function onProgress(ev) {
|
||||||
upload.total = ev.total;
|
upload.total = ev.total;
|
||||||
upload.loaded = ev.loaded;
|
upload.loaded = ev.loaded;
|
||||||
dis.dispatch<UploadProgressPayload>({action: Action.UploadProgress, upload});
|
dis.dispatch<UploadProgressPayload>({ action: Action.UploadProgress, upload });
|
||||||
}
|
}
|
||||||
|
|
||||||
let error;
|
let error;
|
||||||
|
@ -577,11 +599,11 @@ export default class ContentMessages {
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
error = err;
|
error = err;
|
||||||
if (!upload.canceled) {
|
if (!upload.canceled) {
|
||||||
let desc = _t("The file '%(fileName)s' failed to upload.", {fileName: upload.fileName});
|
let desc = _t("The file '%(fileName)s' failed to upload.", { fileName: upload.fileName });
|
||||||
if (err.http_status === 413) {
|
if (err.http_status === 413) {
|
||||||
desc = _t(
|
desc = _t(
|
||||||
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
"The file '%(fileName)s' exceeds this homeserver's size limit for uploads",
|
||||||
{fileName: upload.fileName},
|
{ fileName: upload.fileName },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -604,10 +626,10 @@ export default class ContentMessages {
|
||||||
if (error && error.http_status === 413) {
|
if (error && error.http_status === 413) {
|
||||||
this.mediaConfig = null;
|
this.mediaConfig = null;
|
||||||
}
|
}
|
||||||
dis.dispatch<UploadErrorPayload>({action: Action.UploadFailed, upload, error});
|
dis.dispatch<UploadErrorPayload>({ action: Action.UploadFailed, upload, error });
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch<UploadFinishedPayload>({action: Action.UploadFinished, upload});
|
dis.dispatch<UploadFinishedPayload>({ action: Action.UploadFinished, upload });
|
||||||
dis.dispatch({action: 'message_sent'});
|
dis.dispatch({ action: 'message_sent' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -621,11 +643,11 @@ export default class ContentMessages {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ensureMediaConfigFetched() {
|
private ensureMediaConfigFetched(matrixClient: MatrixClient) {
|
||||||
if (this.mediaConfig !== null) return;
|
if (this.mediaConfig !== null) return;
|
||||||
|
|
||||||
console.log("[Media Config] Fetching");
|
console.log("[Media Config] Fetching");
|
||||||
return MatrixClientPeg.get().getMediaConfig().then((config) => {
|
return matrixClient.getMediaConfig().then((config) => {
|
||||||
console.log("[Media Config] Fetched config:", config);
|
console.log("[Media Config] Fetched config:", config);
|
||||||
return config;
|
return config;
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
|
|
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {randomString} from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
|
import { IContent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import {getCurrentLanguage} from './languageHandler';
|
import { getCurrentLanguage } from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import {sleep} from "./utils/promise";
|
import { sleep } from "./utils/promise";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
|
|
||||||
|
@ -255,7 +256,7 @@ interface ICreateRoomEvent extends IEvent {
|
||||||
num_users: number;
|
num_users: number;
|
||||||
is_encrypted: boolean;
|
is_encrypted: boolean;
|
||||||
is_public: boolean;
|
is_public: boolean;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IJoinRoomEvent extends IEvent {
|
interface IJoinRoomEvent extends IEvent {
|
||||||
|
@ -338,8 +339,8 @@ const getRoomStats = (roomId: string) => {
|
||||||
"is_encrypted": cli?.isRoomEncrypted(roomId),
|
"is_encrypted": cli?.isRoomEncrypted(roomId),
|
||||||
// eslint-disable-next-line camelcase
|
// eslint-disable-next-line camelcase
|
||||||
"is_public": room?.currentState.getStateEvents("m.room.join_rules", "")?.getContent()?.join_rule === "public",
|
"is_public": room?.currentState.getStateEvents("m.room.join_rules", "")?.getContent()?.join_rule === "public",
|
||||||
}
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
// async wrapper for regex-powered String.prototype.replace
|
// async wrapper for regex-powered String.prototype.replace
|
||||||
const strReplaceAsync = async (str: string, regex: RegExp, fn: (...args: string[]) => Promise<string>) => {
|
const strReplaceAsync = async (str: string, regex: RegExp, fn: (...args: string[]) => Promise<string>) => {
|
||||||
|
@ -414,7 +415,7 @@ export default class CountlyAnalytics {
|
||||||
|
|
||||||
this.anonymous = anonymous;
|
this.anonymous = anonymous;
|
||||||
if (anonymous) {
|
if (anonymous) {
|
||||||
await this.changeUserKey(randomString(64))
|
await this.changeUserKey(randomString(64));
|
||||||
} else {
|
} else {
|
||||||
await this.changeUserKey(await hashHex(MatrixClientPeg.get().getUserId()), true);
|
await this.changeUserKey(await hashHex(MatrixClientPeg.get().getUserId()), true);
|
||||||
}
|
}
|
||||||
|
@ -438,7 +439,7 @@ export default class CountlyAnalytics {
|
||||||
await this.track("Opt-Out" );
|
await this.track("Opt-Out" );
|
||||||
this.endSession();
|
this.endSession();
|
||||||
window.clearInterval(this.heartbeatIntervalId);
|
window.clearInterval(this.heartbeatIntervalId);
|
||||||
window.clearTimeout(this.activityIntervalId)
|
window.clearTimeout(this.activityIntervalId);
|
||||||
this.baseUrl = null;
|
this.baseUrl = null;
|
||||||
// remove listeners bound in trackSessions()
|
// remove listeners bound in trackSessions()
|
||||||
window.removeEventListener("beforeunload", this.endSession);
|
window.removeEventListener("beforeunload", this.endSession);
|
||||||
|
@ -662,14 +663,14 @@ export default class CountlyAnalytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
private queue(args: Omit<IEvent, "timestamp" | "hour" | "dow" | "count"> & Partial<Pick<IEvent, "count">>) {
|
private queue(args: Omit<IEvent, "timestamp" | "hour" | "dow" | "count"> & Partial<Pick<IEvent, "count">>) {
|
||||||
const {count = 1, ...rest} = args;
|
const { count = 1, ...rest } = args;
|
||||||
const ev = {
|
const ev = {
|
||||||
...this.getTimeParams(),
|
...this.getTimeParams(),
|
||||||
...rest,
|
...rest,
|
||||||
count,
|
count,
|
||||||
platform: this.appPlatform,
|
platform: this.appPlatform,
|
||||||
app_version: this.appVersion,
|
app_version: this.appVersion,
|
||||||
}
|
};
|
||||||
|
|
||||||
this.pendingEvents.push(ev);
|
this.pendingEvents.push(ev);
|
||||||
if (this.pendingEvents.length > MAX_PENDING_EVENTS) {
|
if (this.pendingEvents.length > MAX_PENDING_EVENTS) {
|
||||||
|
@ -680,7 +681,7 @@ export default class CountlyAnalytics {
|
||||||
private getOrientation = (): Orientation => {
|
private getOrientation = (): Orientation => {
|
||||||
return window.matchMedia("(orientation: landscape)").matches
|
return window.matchMedia("(orientation: landscape)").matches
|
||||||
? Orientation.Landscape
|
? Orientation.Landscape
|
||||||
: Orientation.Portrait
|
: Orientation.Portrait;
|
||||||
};
|
};
|
||||||
|
|
||||||
private reportOrientation = () => {
|
private reportOrientation = () => {
|
||||||
|
@ -749,7 +750,7 @@ export default class CountlyAnalytics {
|
||||||
const request: Parameters<typeof CountlyAnalytics.prototype.request>[0] = {
|
const request: Parameters<typeof CountlyAnalytics.prototype.request>[0] = {
|
||||||
begin_session: 1,
|
begin_session: 1,
|
||||||
user_details: JSON.stringify(userDetails),
|
user_details: JSON.stringify(userDetails),
|
||||||
}
|
};
|
||||||
|
|
||||||
const metrics = this.getMetrics();
|
const metrics = this.getMetrics();
|
||||||
if (metrics) {
|
if (metrics) {
|
||||||
|
@ -773,7 +774,7 @@ export default class CountlyAnalytics {
|
||||||
|
|
||||||
private endSession = () => {
|
private endSession = () => {
|
||||||
if (this.sessionStarted) {
|
if (this.sessionStarted) {
|
||||||
window.removeEventListener("resize", this.reportOrientation)
|
window.removeEventListener("resize", this.reportOrientation);
|
||||||
|
|
||||||
this.reportViewDuration();
|
this.reportViewDuration();
|
||||||
this.request({
|
this.request({
|
||||||
|
@ -868,7 +869,7 @@ export default class CountlyAnalytics {
|
||||||
roomId: string,
|
roomId: string,
|
||||||
isEdit: boolean,
|
isEdit: boolean,
|
||||||
isReply: boolean,
|
isReply: boolean,
|
||||||
content: {format?: string, msgtype: string},
|
content: IContent,
|
||||||
) {
|
) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,34 +14,40 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { MatrixError } from "matrix-js-sdk/src/http-api";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
export class DecryptionFailure {
|
export class DecryptionFailure {
|
||||||
constructor(failedEventId, errorCode) {
|
public readonly ts: number;
|
||||||
this.failedEventId = failedEventId;
|
|
||||||
this.errorCode = errorCode;
|
constructor(public readonly failedEventId: string, public readonly errorCode: string) {
|
||||||
this.ts = Date.now();
|
this.ts = Date.now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TrackingFn = (count: number, trackedErrCode: string) => void;
|
||||||
|
type ErrCodeMapFn = (errcode: string) => string;
|
||||||
|
|
||||||
export class DecryptionFailureTracker {
|
export class DecryptionFailureTracker {
|
||||||
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
|
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
|
||||||
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
|
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
|
||||||
// are accumulated in `failureCounts`.
|
// are accumulated in `failureCounts`.
|
||||||
failures = [];
|
public failures: DecryptionFailure[] = [];
|
||||||
|
|
||||||
// A histogram of the number of failures that will be tracked at the next tracking
|
// A histogram of the number of failures that will be tracked at the next tracking
|
||||||
// interval, split by failure error code.
|
// interval, split by failure error code.
|
||||||
failureCounts = {
|
public failureCounts: Record<string, number> = {
|
||||||
// [errorCode]: 42
|
// [errorCode]: 42
|
||||||
};
|
};
|
||||||
|
|
||||||
// Event IDs of failures that were tracked previously
|
// Event IDs of failures that were tracked previously
|
||||||
trackedEventHashMap = {
|
public trackedEventHashMap: Record<string, boolean> = {
|
||||||
// [eventId]: true
|
// [eventId]: true
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set to an interval ID when `start` is called
|
// Set to an interval ID when `start` is called
|
||||||
checkInterval = null;
|
public checkInterval: NodeJS.Timeout = null;
|
||||||
trackInterval = null;
|
public trackInterval: NodeJS.Timeout = null;
|
||||||
|
|
||||||
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
|
||||||
static TRACK_INTERVAL_MS = 60000;
|
static TRACK_INTERVAL_MS = 60000;
|
||||||
|
@ -67,7 +73,7 @@ export class DecryptionFailureTracker {
|
||||||
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
* @param {function?} errorCodeMapFn The function used to map error codes to the
|
||||||
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
* trackedErrorCode. If not provided, the `.code` of errors will be used.
|
||||||
*/
|
*/
|
||||||
constructor(fn, errorCodeMapFn) {
|
constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
|
||||||
if (!fn || typeof fn !== 'function') {
|
if (!fn || typeof fn !== 'function') {
|
||||||
throw new Error('DecryptionFailureTracker requires tracking function');
|
throw new Error('DecryptionFailureTracker requires tracking function');
|
||||||
}
|
}
|
||||||
|
@ -75,9 +81,6 @@ export class DecryptionFailureTracker {
|
||||||
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
|
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
|
||||||
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
|
throw new Error('DecryptionFailureTracker second constructor argument should be a function');
|
||||||
}
|
}
|
||||||
|
|
||||||
this._trackDecryptionFailure = fn;
|
|
||||||
this._mapErrorCode = errorCodeMapFn;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadTrackedEventHashMap() {
|
// loadTrackedEventHashMap() {
|
||||||
|
@ -88,7 +91,7 @@ export class DecryptionFailureTracker {
|
||||||
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
|
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
eventDecrypted(e, err) {
|
public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void {
|
||||||
if (err) {
|
if (err) {
|
||||||
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
|
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
|
||||||
} else {
|
} else {
|
||||||
|
@ -97,18 +100,18 @@ export class DecryptionFailureTracker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addDecryptionFailure(failure) {
|
public addDecryptionFailure(failure: DecryptionFailure): void {
|
||||||
this.failures.push(failure);
|
this.failures.push(failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeDecryptionFailuresForEvent(e) {
|
public removeDecryptionFailuresForEvent(e: MatrixEvent): void {
|
||||||
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
|
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start checking for and tracking failures.
|
* Start checking for and tracking failures.
|
||||||
*/
|
*/
|
||||||
start() {
|
public start(): void {
|
||||||
this.checkInterval = setInterval(
|
this.checkInterval = setInterval(
|
||||||
() => this.checkFailures(Date.now()),
|
() => this.checkFailures(Date.now()),
|
||||||
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
DecryptionFailureTracker.CHECK_INTERVAL_MS,
|
||||||
|
@ -123,7 +126,7 @@ export class DecryptionFailureTracker {
|
||||||
/**
|
/**
|
||||||
* Clear state and stop checking for and tracking failures.
|
* Clear state and stop checking for and tracking failures.
|
||||||
*/
|
*/
|
||||||
stop() {
|
public stop(): void {
|
||||||
clearInterval(this.checkInterval);
|
clearInterval(this.checkInterval);
|
||||||
clearInterval(this.trackInterval);
|
clearInterval(this.trackInterval);
|
||||||
|
|
||||||
|
@ -132,11 +135,11 @@ export class DecryptionFailureTracker {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be
|
* Mark failures that occurred before nowTs - GRACE_PERIOD_MS as failures that should be
|
||||||
* tracked. Only mark one failure per event ID.
|
* tracked. Only mark one failure per event ID.
|
||||||
* @param {number} nowTs the timestamp that represents the time now.
|
* @param {number} nowTs the timestamp that represents the time now.
|
||||||
*/
|
*/
|
||||||
checkFailures(nowTs) {
|
public checkFailures(nowTs: number): void {
|
||||||
const failuresGivenGrace = [];
|
const failuresGivenGrace = [];
|
||||||
const failuresNotReady = [];
|
const failuresNotReady = [];
|
||||||
while (this.failures.length > 0) {
|
while (this.failures.length > 0) {
|
||||||
|
@ -165,7 +168,7 @@ export class DecryptionFailureTracker {
|
||||||
const trackedEventIds = [...dedupedFailuresMap.keys()];
|
const trackedEventIds = [...dedupedFailuresMap.keys()];
|
||||||
|
|
||||||
this.trackedEventHashMap = trackedEventIds.reduce(
|
this.trackedEventHashMap = trackedEventIds.reduce(
|
||||||
(result, eventId) => ({...result, [eventId]: true}),
|
(result, eventId) => ({ ...result, [eventId]: true }),
|
||||||
this.trackedEventHashMap,
|
this.trackedEventHashMap,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -175,10 +178,10 @@ export class DecryptionFailureTracker {
|
||||||
|
|
||||||
const dedupedFailures = dedupedFailuresMap.values();
|
const dedupedFailures = dedupedFailuresMap.values();
|
||||||
|
|
||||||
this._aggregateFailures(dedupedFailures);
|
this.aggregateFailures(dedupedFailures);
|
||||||
}
|
}
|
||||||
|
|
||||||
_aggregateFailures(failures) {
|
private aggregateFailures(failures: DecryptionFailure[]): void {
|
||||||
for (const failure of failures) {
|
for (const failure of failures) {
|
||||||
const errorCode = failure.errorCode;
|
const errorCode = failure.errorCode;
|
||||||
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
|
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
|
||||||
|
@ -189,12 +192,12 @@ export class DecryptionFailureTracker {
|
||||||
* If there are failures that should be tracked, call the given trackDecryptionFailure
|
* If there are failures that should be tracked, call the given trackDecryptionFailure
|
||||||
* function with the number of failures that should be tracked.
|
* function with the number of failures that should be tracked.
|
||||||
*/
|
*/
|
||||||
trackFailures() {
|
public trackFailures(): void {
|
||||||
for (const errorCode of Object.keys(this.failureCounts)) {
|
for (const errorCode of Object.keys(this.failureCounts)) {
|
||||||
if (this.failureCounts[errorCode] > 0) {
|
if (this.failureCounts[errorCode] > 0) {
|
||||||
const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode;
|
const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode;
|
||||||
|
|
||||||
this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode);
|
this.fn(this.failureCounts[errorCode], trackedErrorCode);
|
||||||
this.failureCounts[errorCode] = 0;
|
this.failureCounts[errorCode] = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import dis from "./dispatcher/dispatcher";
|
import dis from "./dispatcher/dispatcher";
|
||||||
import {
|
import {
|
||||||
hideToast as hideBulkUnverifiedSessionsToast,
|
hideToast as hideBulkUnverifiedSessionsToast,
|
||||||
|
|
|
@ -19,7 +19,7 @@ import Modal from './Modal';
|
||||||
import * as sdk from './';
|
import * as sdk from './';
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import MultiInviter from './utils/MultiInviter';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import GroupStore from './stores/GroupStore';
|
import GroupStore from './stores/GroupStore';
|
||||||
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
import StyledCheckbox from './components/views/elements/StyledCheckbox';
|
||||||
|
|
||||||
|
@ -103,7 +103,7 @@ function _onGroupInviteFinished(groupId, addrs) {
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite the following users to the group', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite the following users to %(groupId)s:", {groupId: groupId}),
|
title: _t("Failed to invite the following users to %(groupId)s:", { groupId: groupId }),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -111,7 +111,7 @@ function _onGroupInviteFinished(groupId, addrs) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite users to group', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite users to community"),
|
title: _t("Failed to invite users to community"),
|
||||||
description: _t("Failed to invite users to %(groupId)s", {groupId: groupId}),
|
description: _t("Failed to invite users to %(groupId)s", { groupId: groupId }),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||||
// Add this group as related
|
// Add this group as related
|
||||||
if (!groups.includes(groupId)) {
|
if (!groups.includes(groupId)) {
|
||||||
groups.push(groupId);
|
groups.push(groupId);
|
||||||
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', {groups}, '');
|
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.related_groups', { groups }, '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})).then(() => {
|
})).then(() => {
|
||||||
|
@ -152,7 +152,7 @@ function _onGroupAddRoomFinished(groupId, addrs, addRoomsPublicly) {
|
||||||
{
|
{
|
||||||
title: _t(
|
title: _t(
|
||||||
"Failed to add the following rooms to %(groupId)s:",
|
"Failed to add the following rooms to %(groupId)s:",
|
||||||
{groupId},
|
{ groupId },
|
||||||
),
|
),
|
||||||
description: errorList.join(", "),
|
description: errorList.join(", "),
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,11 +17,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import sanitizeHtml from 'sanitize-html';
|
import sanitizeHtml from 'sanitize-html';
|
||||||
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
import cheerio from 'cheerio';
|
||||||
import * as linkify from 'linkifyjs';
|
import * as linkify from 'linkifyjs';
|
||||||
import linkifyMatrix from './linkify-matrix';
|
|
||||||
import _linkifyElement from 'linkifyjs/element';
|
import _linkifyElement from 'linkifyjs/element';
|
||||||
import _linkifyString from 'linkifyjs/string';
|
import _linkifyString from 'linkifyjs/string';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
@ -29,13 +28,15 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import katex from 'katex';
|
import katex from 'katex';
|
||||||
import { AllHtmlEntities } from 'html-entities';
|
import { AllHtmlEntities } from 'html-entities';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import { IContent } from 'matrix-js-sdk/src/models/event';
|
||||||
import cheerio from 'cheerio';
|
|
||||||
|
|
||||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
||||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
import linkifyMatrix from './linkify-matrix';
|
||||||
|
import SettingsStore from './settings/SettingsStore';
|
||||||
|
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
|
||||||
|
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
import {mediaFromMxc} from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
|
||||||
linkifyMatrix(linkify);
|
linkifyMatrix(linkify);
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'
|
||||||
* need emojification.
|
* need emojification.
|
||||||
* unicodeToImage uses this function.
|
* unicodeToImage uses this function.
|
||||||
*/
|
*/
|
||||||
function mightContainEmoji(str: string) {
|
function mightContainEmoji(str: string): boolean {
|
||||||
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ function mightContainEmoji(str: string) {
|
||||||
* @param {String} char The emoji character
|
* @param {String} char The emoji character
|
||||||
* @return {String} The shortcode (such as :thumbup:)
|
* @return {String} The shortcode (such as :thumbup:)
|
||||||
*/
|
*/
|
||||||
export function unicodeToShortcode(char: string) {
|
export function unicodeToShortcode(char: string): string {
|
||||||
const data = getEmojiFromUnicode(char);
|
const data = getEmojiFromUnicode(char);
|
||||||
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
|
||||||
}
|
}
|
||||||
|
@ -87,7 +88,7 @@ export function unicodeToShortcode(char: string) {
|
||||||
* @param {String} shortcode The shortcode (such as :thumbup:)
|
* @param {String} shortcode The shortcode (such as :thumbup:)
|
||||||
* @return {String} The emoji character; null if none exists
|
* @return {String} The emoji character; null if none exists
|
||||||
*/
|
*/
|
||||||
export function shortcodeToUnicode(shortcode: string) {
|
export function shortcodeToUnicode(shortcode: string): string {
|
||||||
shortcode = shortcode.slice(1, shortcode.length - 1);
|
shortcode = shortcode.slice(1, shortcode.length - 1);
|
||||||
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
const data = SHORTCODE_TO_EMOJI.get(shortcode);
|
||||||
return data ? data.unicode : null;
|
return data ? data.unicode : null;
|
||||||
|
@ -124,20 +125,20 @@ export function processHtmlForSending(html: string): string {
|
||||||
* Given an untrusted HTML string, return a React node with an sanitized version
|
* Given an untrusted HTML string, return a React node with an sanitized version
|
||||||
* of that HTML.
|
* of that HTML.
|
||||||
*/
|
*/
|
||||||
export function sanitizedHtmlNode(insaneHtml: string) {
|
export function sanitizedHtmlNode(insaneHtml: string): ReactNode {
|
||||||
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
|
||||||
|
|
||||||
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHtmlText(insaneHtml: string) {
|
export function getHtmlText(insaneHtml: string): string {
|
||||||
return sanitizeHtml(insaneHtml, {
|
return sanitizeHtml(insaneHtml, {
|
||||||
allowedTags: [],
|
allowedTags: [],
|
||||||
allowedAttributes: {},
|
allowedAttributes: {},
|
||||||
selfClosing: [],
|
selfClosing: [],
|
||||||
allowedSchemes: [],
|
allowedSchemes: [],
|
||||||
disallowedTagsMode: 'discard',
|
disallowedTagsMode: 'discard',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -148,7 +149,7 @@ export function getHtmlText(insaneHtml: string) {
|
||||||
* other places we need to sanitise URLs.
|
* other places we need to sanitise URLs.
|
||||||
* @return true if permitted, otherwise false
|
* @return true if permitted, otherwise false
|
||||||
*/
|
*/
|
||||||
export function isUrlPermitted(inputUrl: string) {
|
export function isUrlPermitted(inputUrl: string): boolean {
|
||||||
try {
|
try {
|
||||||
const parsed = url.parse(inputUrl);
|
const parsed = url.parse(inputUrl);
|
||||||
if (!parsed.protocol) return false;
|
if (!parsed.protocol) return false;
|
||||||
|
@ -182,7 +183,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
||||||
// images" preference is disabled. Future work might expose some UI to reveal them
|
// images" preference is disabled. Future work might expose some UI to reveal them
|
||||||
// like standalone image events have.
|
// like standalone image events have.
|
||||||
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
||||||
return { tagName, attribs: {}};
|
return { tagName, attribs: {} };
|
||||||
}
|
}
|
||||||
const width = Number(attribs.width) || 800;
|
const width = Number(attribs.width) || 800;
|
||||||
const height = Number(attribs.height) || 600;
|
const height = Number(attribs.height) || 600;
|
||||||
|
@ -351,20 +352,21 @@ class HtmlHighlighter extends BaseHighlighter<string> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IContent {
|
|
||||||
format?: string;
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
formatted_body?: string;
|
|
||||||
body: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IOpts {
|
interface IOpts {
|
||||||
highlightLink?: string;
|
highlightLink?: string;
|
||||||
disableBigEmoji?: boolean;
|
disableBigEmoji?: boolean;
|
||||||
stripReplyFallback?: boolean;
|
stripReplyFallback?: boolean;
|
||||||
returnString?: boolean;
|
returnString?: boolean;
|
||||||
forComposerQuote?: boolean;
|
forComposerQuote?: boolean;
|
||||||
ref?: React.Ref<any>;
|
ref?: React.Ref<HTMLSpanElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOptsReturnNode extends IOpts {
|
||||||
|
returnString: false | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOptsReturnString extends IOpts {
|
||||||
|
returnString: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* turn a matrix event body into html
|
/* turn a matrix event body into html
|
||||||
|
@ -380,6 +382,8 @@ interface IOpts {
|
||||||
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
|
||||||
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
|
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
|
||||||
*/
|
*/
|
||||||
|
export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnString): string;
|
||||||
|
export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnNode): ReactNode;
|
||||||
export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) {
|
export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) {
|
||||||
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
|
||||||
let bodyHasEmoji = false;
|
let bodyHasEmoji = false;
|
||||||
|
@ -399,9 +403,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||||
try {
|
try {
|
||||||
if (highlights && highlights.length > 0) {
|
if (highlights && highlights.length > 0) {
|
||||||
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
const highlighter = new HtmlHighlighter("mx_EventTile_searchHighlight", opts.highlightLink);
|
||||||
const safeHighlights = highlights.map(function(highlight) {
|
const safeHighlights = highlights
|
||||||
return sanitizeHtml(highlight, sanitizeParams);
|
// sanitizeHtml can hang if an unclosed HTML tag is thrown at it
|
||||||
});
|
// A search for `<foo` will make the browser crash
|
||||||
|
// an alternative would be to escape HTML special characters
|
||||||
|
// but that would bring no additional benefit as the highlighter
|
||||||
|
// does not work with those special chars
|
||||||
|
.filter((highlight: string): boolean => !highlight.includes("<"))
|
||||||
|
.map((highlight: string): string => sanitizeHtml(highlight, sanitizeParams));
|
||||||
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
|
// XXX: hacky bodge to temporarily apply a textFilter to the sanitizeParams structure.
|
||||||
sanitizeParams.textFilter = function(safeText) {
|
sanitizeParams.textFilter = function(safeText) {
|
||||||
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
return highlighter.applyHighlights(safeText, safeHighlights).join('');
|
||||||
|
@ -501,7 +510,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
||||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||||
* @returns {string} Linkified string
|
* @returns {string} Linkified string
|
||||||
*/
|
*/
|
||||||
export function linkifyString(str: string, options = linkifyMatrix.options) {
|
export function linkifyString(str: string, options = linkifyMatrix.options): string {
|
||||||
return _linkifyString(str, options);
|
return _linkifyString(str, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -512,7 +521,7 @@ export function linkifyString(str: string, options = linkifyMatrix.options) {
|
||||||
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
|
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
|
||||||
* @returns {object}
|
* @returns {object}
|
||||||
*/
|
*/
|
||||||
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) {
|
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement {
|
||||||
return _linkifyElement(element, options);
|
return _linkifyElement(element, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -523,7 +532,7 @@ export function linkifyElement(element: HTMLElement, options = linkifyMatrix.opt
|
||||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) {
|
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string {
|
||||||
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -534,7 +543,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatri
|
||||||
* @param {Node} node
|
* @param {Node} node
|
||||||
* @returns {bool}
|
* @returns {bool}
|
||||||
*/
|
*/
|
||||||
export function checkBlockNode(node: Node) {
|
export function checkBlockNode(node: Node): boolean {
|
||||||
switch (node.nodeName) {
|
switch (node.nodeName) {
|
||||||
case "H1":
|
case "H1":
|
||||||
case "H2":
|
case "H2":
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
|
@ -156,7 +156,7 @@ const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bindings;
|
return bindings;
|
||||||
}
|
};
|
||||||
|
|
||||||
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
|
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
|
||||||
return [
|
return [
|
||||||
|
@ -207,7 +207,7 @@ const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
const roomListBindings = (): KeyBinding<RoomListAction>[] => {
|
const roomListBindings = (): KeyBinding<RoomListAction>[] => {
|
||||||
return [
|
return [
|
||||||
|
@ -248,7 +248,7 @@ const roomListBindings = (): KeyBinding<RoomListAction>[] => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
const roomBindings = (): KeyBinding<RoomAction>[] => {
|
const roomBindings = (): KeyBinding<RoomAction>[] => {
|
||||||
const bindings: KeyBinding<RoomAction>[] = [
|
const bindings: KeyBinding<RoomAction>[] = [
|
||||||
|
@ -312,7 +312,7 @@ const roomBindings = (): KeyBinding<RoomAction>[] => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return bindings;
|
return bindings;
|
||||||
}
|
};
|
||||||
|
|
||||||
const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
||||||
return [
|
return [
|
||||||
|
@ -396,7 +396,7 @@ const navigationBindings = (): KeyBinding<NavigationAction>[] => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
};
|
||||||
|
|
||||||
export const defaultBindingsProvider: IKeyBindingsProvider = {
|
export const defaultBindingsProvider: IKeyBindingsProvider = {
|
||||||
getMessageComposerBindings: messageComposerBindings,
|
getMessageComposerBindings: messageComposerBindings,
|
||||||
|
@ -404,4 +404,4 @@ export const defaultBindingsProvider: IKeyBindingsProvider = {
|
||||||
getRoomListBindings: roomListBindings,
|
getRoomListBindings: roomListBindings,
|
||||||
getRoomBindings: roomBindings,
|
getRoomBindings: roomBindings,
|
||||||
getNavigationBindings: navigationBindings,
|
getNavigationBindings: navigationBindings,
|
||||||
}
|
};
|
||||||
|
|
|
@ -140,12 +140,12 @@ export type KeyCombo = {
|
||||||
ctrlKey?: boolean;
|
ctrlKey?: boolean;
|
||||||
metaKey?: boolean;
|
metaKey?: boolean;
|
||||||
shiftKey?: boolean;
|
shiftKey?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type KeyBinding<T extends string> = {
|
export type KeyBinding<T extends string> = {
|
||||||
action: T;
|
action: T;
|
||||||
keyCombo: KeyCombo;
|
keyCombo: KeyCombo;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper method to check if a KeyboardEvent matches a KeyCombo
|
* Helper method to check if a KeyboardEvent matches a KeyCombo
|
||||||
|
|
|
@ -20,9 +20,9 @@ limitations under the License.
|
||||||
import { createClient } from 'matrix-js-sdk/src/matrix';
|
import { createClient } from 'matrix-js-sdk/src/matrix';
|
||||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
|
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
|
||||||
|
|
||||||
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
import EventIndexPeg from './indexing/EventIndexPeg';
|
import EventIndexPeg from './indexing/EventIndexPeg';
|
||||||
import createMatrixClient from './utils/createMatrixClient';
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
|
@ -41,17 +41,17 @@ import * as StorageManager from './utils/StorageManager';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import TypingStore from "./stores/TypingStore";
|
import TypingStore from "./stores/TypingStore";
|
||||||
import ToastStore from "./stores/ToastStore";
|
import ToastStore from "./stores/ToastStore";
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
import { IntegrationManagers } from "./integrations/IntegrationManagers";
|
||||||
import {Mjolnir} from "./mjolnir/Mjolnir";
|
import { Mjolnir } from "./mjolnir/Mjolnir";
|
||||||
import DeviceListener from "./DeviceListener";
|
import DeviceListener from "./DeviceListener";
|
||||||
import {Jitsi} from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY} from "./BasePlatform";
|
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
|
||||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||||
import CountlyAnalytics from "./CountlyAnalytics";
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
import CallHandler from './CallHandler';
|
import CallHandler from './CallHandler';
|
||||||
import LifecycleCustomisations from "./customisations/Lifecycle";
|
import LifecycleCustomisations from "./customisations/Lifecycle";
|
||||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import {_t} from "./languageHandler";
|
import { _t } from "./languageHandler";
|
||||||
|
|
||||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||||
|
@ -154,7 +154,7 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
|
||||||
* return [null, null].
|
* return [null, null].
|
||||||
*/
|
*/
|
||||||
export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
export async function getStoredSessionOwner(): Promise<[string, boolean]> {
|
||||||
const {hsUrl, userId, hasAccessToken, isGuest} = await getStoredSessionVars();
|
const { hsUrl, userId, hasAccessToken, isGuest } = await getStoredSessionVars();
|
||||||
return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null];
|
return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,7 +303,7 @@ export interface IStoredSession {
|
||||||
hsUrl: string;
|
hsUrl: string;
|
||||||
isUrl: string;
|
isUrl: string;
|
||||||
hasAccessToken: boolean;
|
hasAccessToken: boolean;
|
||||||
accessToken: string | object;
|
accessToken: string | IEncryptedPayload;
|
||||||
userId: string;
|
userId: string;
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
isGuest: boolean;
|
isGuest: boolean;
|
||||||
|
@ -346,11 +346,11 @@ export async function getStoredSessionVars(): Promise<IStoredSession> {
|
||||||
isGuest = localStorage.getItem("matrix-is-guest") === "true";
|
isGuest = localStorage.getItem("matrix-is-guest") === "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
return {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest};
|
return { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest };
|
||||||
}
|
}
|
||||||
|
|
||||||
// The pickle key is a string of unspecified length and format. For AES, we
|
// The pickle key is a string of unspecified length and format. For AES, we
|
||||||
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
|
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
|
||||||
// key. The AES key should be zeroed after it is used.
|
// key. The AES key should be zeroed after it is used.
|
||||||
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
|
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
|
||||||
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
|
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
|
||||||
|
@ -402,7 +402,7 @@ export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }):
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest} = await getStoredSessionVars();
|
const { hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest } = await getStoredSessionVars();
|
||||||
|
|
||||||
if (hasAccessToken && !accessToken) {
|
if (hasAccessToken && !accessToken) {
|
||||||
abortLogin();
|
abortLogin();
|
||||||
|
@ -495,7 +495,7 @@ export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<Matr
|
||||||
console.log("Pickle key not created");
|
console.log("Pickle key not created");
|
||||||
}
|
}
|
||||||
|
|
||||||
return doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
return doSetLoggedIn(Object.assign({}, credentials, { pickleKey }), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -562,7 +562,7 @@ async function doSetLoggedIn(
|
||||||
//
|
//
|
||||||
// we fire it *synchronously* to make sure it fires before on_logged_in.
|
// we fire it *synchronously* to make sure it fires before on_logged_in.
|
||||||
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
|
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
|
||||||
dis.dispatch({action: 'on_logging_in'}, true);
|
dis.dispatch({ action: 'on_logging_in' }, true);
|
||||||
|
|
||||||
if (clearStorageEnabled) {
|
if (clearStorageEnabled) {
|
||||||
await clearStorage();
|
await clearStorage();
|
||||||
|
@ -745,7 +745,7 @@ export function softLogout(): void {
|
||||||
// Ensure that we dispatch a view change **before** stopping the client so
|
// Ensure that we dispatch a view change **before** stopping the client so
|
||||||
// so that React components unmount first. This avoids React soft crashes
|
// so that React components unmount first. This avoids React soft crashes
|
||||||
// that can occur when components try to use a null client.
|
// that can occur when components try to use a null client.
|
||||||
dis.dispatch({action: 'on_client_not_viable'}); // generic version of on_logged_out
|
dis.dispatch({ action: 'on_client_not_viable' }); // generic version of on_logged_out
|
||||||
stopMatrixClient(/*unsetClient=*/false);
|
stopMatrixClient(/*unsetClient=*/false);
|
||||||
|
|
||||||
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
|
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
|
||||||
|
@ -772,7 +772,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||||
// to add listeners for the 'sync' event so otherwise we'd have
|
// to add listeners for the 'sync' event so otherwise we'd have
|
||||||
// a race condition (and we need to dispatch synchronously for this
|
// a race condition (and we need to dispatch synchronously for this
|
||||||
// to work).
|
// to work).
|
||||||
dis.dispatch({action: 'will_start_client'}, true);
|
dis.dispatch({ action: 'will_start_client' }, true);
|
||||||
|
|
||||||
// reset things first just in case
|
// reset things first just in case
|
||||||
TypingStore.sharedInstance().reset();
|
TypingStore.sharedInstance().reset();
|
||||||
|
@ -814,7 +814,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||||
|
|
||||||
// dispatch that we finished starting up to wire up any other bits
|
// dispatch that we finished starting up to wire up any other bits
|
||||||
// of the matrix client that cannot be set prior to starting up.
|
// of the matrix client that cannot be set prior to starting up.
|
||||||
dis.dispatch({action: 'client_started'});
|
dis.dispatch({ action: 'client_started' });
|
||||||
|
|
||||||
if (isSoftLogout()) {
|
if (isSoftLogout()) {
|
||||||
softLogout();
|
softLogout();
|
||||||
|
@ -830,9 +830,9 @@ export async function onLoggedOut(): Promise<void> {
|
||||||
// Ensure that we dispatch a view change **before** stopping the client so
|
// Ensure that we dispatch a view change **before** stopping the client so
|
||||||
// so that React components unmount first. This avoids React soft crashes
|
// so that React components unmount first. This avoids React soft crashes
|
||||||
// that can occur when components try to use a null client.
|
// that can occur when components try to use a null client.
|
||||||
dis.dispatch({action: 'on_logged_out'}, true);
|
dis.dispatch({ action: 'on_logged_out' }, true);
|
||||||
stopMatrixClient();
|
stopMatrixClient();
|
||||||
await clearStorage({deleteEverything: true});
|
await clearStorage({ deleteEverything: true });
|
||||||
LifecycleCustomisations.onLoggedOutAndStorageCleared?.();
|
LifecycleCustomisations.onLoggedOutAndStorageCleared?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||||
import {createClient} from "matrix-js-sdk/src/matrix";
|
import { createClient } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
@ -190,7 +190,6 @@ export default class Login {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a login request to the given server, and format the response
|
* Send a login request to the given server, and format the response
|
||||||
* as a MatrixClientCreds
|
* as a MatrixClientCreds
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,16 +16,24 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as commonmark from 'commonmark';
|
import * as commonmark from 'commonmark';
|
||||||
import {escape} from "lodash";
|
import { escape } from "lodash";
|
||||||
|
|
||||||
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];
|
||||||
|
|
||||||
// These types of node are definitely text
|
// These types of node are definitely text
|
||||||
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
const TEXT_NODES = ['text', 'softbreak', 'linebreak', 'paragraph', 'document'];
|
||||||
|
|
||||||
function is_allowed_html_tag(node) {
|
// As far as @types/commonmark is concerned, these are not public, so add them
|
||||||
|
interface CommonmarkHtmlRendererInternal extends commonmark.HtmlRenderer {
|
||||||
|
paragraph: (node: commonmark.Node, entering: boolean) => void;
|
||||||
|
link: (node: commonmark.Node, entering: boolean) => void;
|
||||||
|
html_inline: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||||
|
html_block: (node: commonmark.Node) => void; // eslint-disable-line camelcase
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAllowedHtmlTag(node: commonmark.Node): boolean {
|
||||||
if (node.literal != null &&
|
if (node.literal != null &&
|
||||||
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|\/(div|span))>$') != null) {
|
node.literal.match('^<((div|span) data-mx-maths="[^"]*"|/(div|span))>$') != null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,21 +48,12 @@ function is_allowed_html_tag(node) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function html_if_tag_allowed(node) {
|
|
||||||
if (is_allowed_html_tag(node)) {
|
|
||||||
this.lit(node.literal);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
this.lit(escape(node.literal));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Returns true if the parse output containing the node
|
* Returns true if the parse output containing the node
|
||||||
* comprises multiple block level elements (ie. lines),
|
* comprises multiple block level elements (ie. lines),
|
||||||
* or false if it is only a single line.
|
* or false if it is only a single line.
|
||||||
*/
|
*/
|
||||||
function is_multi_line(node) {
|
function isMultiLine(node: commonmark.Node): boolean {
|
||||||
let par = node;
|
let par = node;
|
||||||
while (par.parent) {
|
while (par.parent) {
|
||||||
par = par.parent;
|
par = par.parent;
|
||||||
|
@ -67,6 +67,9 @@ function is_multi_line(node) {
|
||||||
* it's plain text.
|
* it's plain text.
|
||||||
*/
|
*/
|
||||||
export default class Markdown {
|
export default class Markdown {
|
||||||
|
private input: string;
|
||||||
|
private parsed: commonmark.Node;
|
||||||
|
|
||||||
constructor(input) {
|
constructor(input) {
|
||||||
this.input = input;
|
this.input = input;
|
||||||
|
|
||||||
|
@ -74,7 +77,7 @@ export default class Markdown {
|
||||||
this.parsed = parser.parse(this.input);
|
this.parsed = parser.parse(this.input);
|
||||||
}
|
}
|
||||||
|
|
||||||
isPlainText() {
|
isPlainText(): boolean {
|
||||||
const walker = this.parsed.walker();
|
const walker = this.parsed.walker();
|
||||||
|
|
||||||
let ev;
|
let ev;
|
||||||
|
@ -87,7 +90,7 @@ export default class Markdown {
|
||||||
// if it's an allowed html tag, we need to render it and therefore
|
// if it's an allowed html tag, we need to render it and therefore
|
||||||
// we will need to use HTML. If it's not allowed, it's not HTML since
|
// we will need to use HTML. If it's not allowed, it's not HTML since
|
||||||
// we'll just be treating it as text.
|
// we'll just be treating it as text.
|
||||||
if (is_allowed_html_tag(node)) {
|
if (isAllowedHtmlTag(node)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -97,7 +100,7 @@ export default class Markdown {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
toHTML({ externalLinks = false } = {}) {
|
toHTML({ externalLinks = false } = {}): string {
|
||||||
const renderer = new commonmark.HtmlRenderer({
|
const renderer = new commonmark.HtmlRenderer({
|
||||||
safe: false,
|
safe: false,
|
||||||
|
|
||||||
|
@ -107,7 +110,7 @@ export default class Markdown {
|
||||||
// block quote ends up all on one line
|
// block quote ends up all on one line
|
||||||
// (https://github.com/vector-im/element-web/issues/3154)
|
// (https://github.com/vector-im/element-web/issues/3154)
|
||||||
softbreak: '<br />',
|
softbreak: '<br />',
|
||||||
});
|
}) as CommonmarkHtmlRendererInternal;
|
||||||
|
|
||||||
// Trying to strip out the wrapping <p/> causes a lot more complication
|
// Trying to strip out the wrapping <p/> causes a lot more complication
|
||||||
// than it's worth, i think. For instance, this code will go and strip
|
// than it's worth, i think. For instance, this code will go and strip
|
||||||
|
@ -118,16 +121,16 @@ export default class Markdown {
|
||||||
//
|
//
|
||||||
// Let's try sending with <p/>s anyway for now, though.
|
// Let's try sending with <p/>s anyway for now, though.
|
||||||
|
|
||||||
const real_paragraph = renderer.paragraph;
|
const realParagraph = renderer.paragraph;
|
||||||
|
|
||||||
renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||||
// If there is only one top level node, just return the
|
// If there is only one top level node, just return the
|
||||||
// bare text: it's a single line of text and so should be
|
// bare text: it's a single line of text and so should be
|
||||||
// 'inline', rather than unnecessarily wrapped in its own
|
// 'inline', rather than unnecessarily wrapped in its own
|
||||||
// p tag. If, however, we have multiple nodes, each gets
|
// p tag. If, however, we have multiple nodes, each gets
|
||||||
// its own p tag to keep them as separate paragraphs.
|
// its own p tag to keep them as separate paragraphs.
|
||||||
if (is_multi_line(node)) {
|
if (isMultiLine(node)) {
|
||||||
real_paragraph.call(this, node, entering);
|
realParagraph.call(this, node, entering);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -150,19 +153,26 @@ export default class Markdown {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.html_inline = html_if_tag_allowed;
|
renderer.html_inline = function(node: commonmark.Node) {
|
||||||
|
if (isAllowedHtmlTag(node)) {
|
||||||
|
this.lit(node.literal);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.lit(escape(node.literal));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
renderer.html_block = function(node) {
|
renderer.html_block = function(node: commonmark.Node) {
|
||||||
/*
|
/*
|
||||||
// as with `paragraph`, we only insert line breaks
|
// as with `paragraph`, we only insert line breaks
|
||||||
// if there are multiple lines in the markdown.
|
// if there are multiple lines in the markdown.
|
||||||
const isMultiLine = is_multi_line(node);
|
const isMultiLine = is_multi_line(node);
|
||||||
if (isMultiLine) this.cr();
|
if (isMultiLine) this.cr();
|
||||||
*/
|
*/
|
||||||
html_if_tag_allowed.call(this, node);
|
renderer.html_inline(node);
|
||||||
/*
|
/*
|
||||||
if (isMultiLine) this.cr();
|
if (isMultiLine) this.cr();
|
||||||
*/
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
return renderer.render(this.parsed);
|
return renderer.render(this.parsed);
|
||||||
|
@ -177,23 +187,22 @@ export default class Markdown {
|
||||||
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
* N.B. this does **NOT** render arbitrary MD to plain text - only MD
|
||||||
* which has no formatting. Otherwise it emits HTML(!).
|
* which has no formatting. Otherwise it emits HTML(!).
|
||||||
*/
|
*/
|
||||||
toPlaintext() {
|
toPlaintext(): string {
|
||||||
const renderer = new commonmark.HtmlRenderer({safe: false});
|
const renderer = new commonmark.HtmlRenderer({ safe: false }) as CommonmarkHtmlRendererInternal;
|
||||||
const real_paragraph = renderer.paragraph;
|
|
||||||
|
|
||||||
renderer.paragraph = function(node, entering) {
|
renderer.paragraph = function(node: commonmark.Node, entering: boolean) {
|
||||||
// as with toHTML, only append lines to paragraphs if there are
|
// as with toHTML, only append lines to paragraphs if there are
|
||||||
// multiple paragraphs
|
// multiple paragraphs
|
||||||
if (is_multi_line(node)) {
|
if (isMultiLine(node)) {
|
||||||
if (!entering && node.next) {
|
if (!entering && node.next) {
|
||||||
this.lit('\n\n');
|
this.lit('\n\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
renderer.html_block = function(node) {
|
renderer.html_block = function(node: commonmark.Node) {
|
||||||
this.lit(node.literal);
|
this.lit(node.literal);
|
||||||
if (is_multi_line(node) && node.next) this.lit('\n\n');
|
if (isMultiLine(node) && node.next) this.lit('\n\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
return renderer.render(this.parsed);
|
return renderer.render(this.parsed);
|
|
@ -18,22 +18,22 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix';
|
import { ICreateClientOpts } from 'matrix-js-sdk/src/matrix';
|
||||||
import {MatrixClient} from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import {MemoryStore} from 'matrix-js-sdk/src/store/memory';
|
import { MemoryStore } from 'matrix-js-sdk/src/store/memory';
|
||||||
import * as utils from 'matrix-js-sdk/src/utils';
|
import * as utils from 'matrix-js-sdk/src/utils';
|
||||||
import {EventTimeline} from 'matrix-js-sdk/src/models/event-timeline';
|
import { EventTimeline } from 'matrix-js-sdk/src/models/event-timeline';
|
||||||
import {EventTimelineSet} from 'matrix-js-sdk/src/models/event-timeline-set';
|
import { EventTimelineSet } from 'matrix-js-sdk/src/models/event-timeline-set';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import createMatrixClient from './utils/createMatrixClient';
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
import SettingsStore from './settings/SettingsStore';
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import MatrixActionCreators from './actions/MatrixActionCreators';
|
import MatrixActionCreators from './actions/MatrixActionCreators';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
import { verificationMethods } from 'matrix-js-sdk/src/crypto';
|
||||||
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientBackedSettingsHandler";
|
||||||
import * as StorageManager from './utils/StorageManager';
|
import * as StorageManager from './utils/StorageManager';
|
||||||
import IdentityAuthClient from './IdentityAuthClient';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
||||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
import { SHOW_QR_CODE_METHOD } from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
|
||||||
export interface IMatrixClientCreds {
|
export interface IMatrixClientCreds {
|
||||||
|
|
120
src/MediaDeviceHandler.ts
Normal file
|
@ -0,0 +1,120 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
|
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
interface IMediaDevices {
|
||||||
|
audioOutput: Array<MediaDeviceInfo>;
|
||||||
|
audioInput: Array<MediaDeviceInfo>;
|
||||||
|
videoInput: Array<MediaDeviceInfo>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum MediaDeviceHandlerEvent {
|
||||||
|
AudioOutputChanged = "audio_output_changed",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class MediaDeviceHandler extends EventEmitter {
|
||||||
|
private static internalInstance;
|
||||||
|
|
||||||
|
public static get instance(): MediaDeviceHandler {
|
||||||
|
if (!MediaDeviceHandler.internalInstance) {
|
||||||
|
MediaDeviceHandler.internalInstance = new MediaDeviceHandler();
|
||||||
|
}
|
||||||
|
return MediaDeviceHandler.internalInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async hasAnyLabeledDevices(): Promise<boolean> {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
return devices.some(d => Boolean(d.label));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getDevices(): Promise<IMediaDevices> {
|
||||||
|
// Only needed for Electron atm, though should work in modern browsers
|
||||||
|
// once permission has been granted to the webapp
|
||||||
|
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||||
|
|
||||||
|
const audioOutput = [];
|
||||||
|
const audioInput = [];
|
||||||
|
const videoInput = [];
|
||||||
|
|
||||||
|
devices.forEach((device) => {
|
||||||
|
switch (device.kind) {
|
||||||
|
case 'audiooutput': audioOutput.push(device); break;
|
||||||
|
case 'audioinput': audioInput.push(device); break;
|
||||||
|
case 'videoinput': videoInput.push(device); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { audioOutput, audioInput, videoInput };
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Unable to refresh WebRTC Devices: ', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
|
||||||
|
*/
|
||||||
|
public static loadDevices(): void {
|
||||||
|
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
|
||||||
|
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
|
||||||
|
|
||||||
|
setMatrixCallAudioInput(audioDeviceId);
|
||||||
|
setMatrixCallVideoInput(videoDeviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAudioOutput(deviceId: string): void {
|
||||||
|
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will not change the device that a potential call uses. The call will
|
||||||
|
* need to be ended and started again for this change to take effect
|
||||||
|
* @param {string} deviceId
|
||||||
|
*/
|
||||||
|
public setAudioInput(deviceId: string): void {
|
||||||
|
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
setMatrixCallAudioInput(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will not change the device that a potential call uses. The call will
|
||||||
|
* need to be ended and started again for this change to take effect
|
||||||
|
* @param {string} deviceId
|
||||||
|
*/
|
||||||
|
public setVideoInput(deviceId: string): void {
|
||||||
|
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
|
||||||
|
setMatrixCallVideoInput(deviceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAudioOutput(): string {
|
||||||
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getAudioInput(): string {
|
||||||
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getVideoInput(): string {
|
||||||
|
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,14 +15,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import {defer} from './utils/promise';
|
import { defer } from './utils/promise';
|
||||||
import AsyncWrapper from './AsyncWrapper';
|
import AsyncWrapper from './AsyncWrapper';
|
||||||
|
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
|
@ -193,7 +192,7 @@ export class ModalManager {
|
||||||
modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
|
modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
|
||||||
modal.close = closeDialog;
|
modal.close = closeDialog;
|
||||||
|
|
||||||
return {modal, closeDialog, onFinishedProm};
|
return { modal, closeDialog, onFinishedProm };
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCloseFn<T extends any[]>(
|
private getCloseFn<T extends any[]>(
|
||||||
|
@ -282,7 +281,7 @@ export class ModalManager {
|
||||||
isStaticModal = false,
|
isStaticModal = false,
|
||||||
options: IOptions<T> = {},
|
options: IOptions<T> = {},
|
||||||
): IHandle<T> {
|
): IHandle<T> {
|
||||||
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, options);
|
const { modal, closeDialog, onFinishedProm } = this.buildModal<T>(prom, props, className, options);
|
||||||
if (isPriorityModal) {
|
if (isPriorityModal) {
|
||||||
// XXX: This is destructive
|
// XXX: This is destructive
|
||||||
this.priorityModal = modal;
|
this.priorityModal = modal;
|
||||||
|
@ -305,7 +304,7 @@ export class ModalManager {
|
||||||
props?: IProps<T>,
|
props?: IProps<T>,
|
||||||
className?: string,
|
className?: string,
|
||||||
): IHandle<T> {
|
): IHandle<T> {
|
||||||
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, {});
|
const { modal, closeDialog, onFinishedProm } = this.buildModal<T>(prom, props, className, {});
|
||||||
|
|
||||||
this.modals.push(modal);
|
this.modals.push(modal);
|
||||||
this.reRender();
|
this.reRender();
|
||||||
|
@ -385,7 +384,7 @@ export class ModalManager {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
ReactDOM.render(dialog, ModalManager.getOrCreateContainer());
|
setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()));
|
||||||
} else {
|
} else {
|
||||||
// This is safe to call repeatedly if we happen to do that
|
// This is safe to call repeatedly if we happen to do that
|
||||||
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
|
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
|
||||||
|
|
|
@ -32,11 +32,11 @@ import { _t } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
|
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
|
||||||
import {SettingLevel} from "./settings/SettingLevel";
|
import { SettingLevel } from "./settings/SettingLevel";
|
||||||
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
|
import { isPushNotifyDisabled } from "./settings/controllers/NotificationControllers";
|
||||||
import RoomViewStore from "./stores/RoomViewStore";
|
import RoomViewStore from "./stores/RoomViewStore";
|
||||||
import UserActivity from "./UserActivity";
|
import UserActivity from "./UserActivity";
|
||||||
import {mediaFromMxc} from "./customisations/Media";
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Dispatches:
|
* Dispatches:
|
||||||
|
@ -68,7 +68,7 @@ export const Notifier = {
|
||||||
// or not
|
// or not
|
||||||
pendingEncryptedEventIds: [],
|
pendingEncryptedEventIds: [],
|
||||||
|
|
||||||
notificationMessageForEvent: function(ev: MatrixEvent) {
|
notificationMessageForEvent: function(ev: MatrixEvent): string {
|
||||||
if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
|
if (typehandlers.hasOwnProperty(ev.getContent().msgtype)) {
|
||||||
return typehandlers[ev.getContent().msgtype](ev);
|
return typehandlers[ev.getContent().msgtype](ev);
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import dis from "./dispatcher/dispatcher";
|
import dis from "./dispatcher/dispatcher";
|
||||||
import Timer from './utils/Timer';
|
import Timer from './utils/Timer';
|
||||||
import {ActionPayload} from "./dispatcher/payloads";
|
import { ActionPayload } from "./dispatcher/payloads";
|
||||||
|
|
||||||
// Time in ms after that a user is considered as unavailable/away
|
// Time in ms after that a user is considered as unavailable/away
|
||||||
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||||
|
@ -78,7 +78,7 @@ class Presence {
|
||||||
this.setState(State.Online);
|
this.setState(State.Online);
|
||||||
this.unavailableTimer.restart();
|
this.unavailableTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the presence state.
|
* Set the presence state.
|
||||||
|
@ -98,7 +98,7 @@ class Presence {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await MatrixClientPeg.get().setPresence({presence: this.state});
|
await MatrixClientPeg.get().setPresence({ presence: this.state });
|
||||||
console.info("Presence:", newState);
|
console.info("Presence:", newState);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to set presence:", err);
|
console.error("Failed to set presence:", err);
|
||||||
|
|
|
@ -53,16 +53,16 @@ export async function startAnyRegistrationFlow(options) {
|
||||||
extraButtons: [
|
extraButtons: [
|
||||||
<button key="start_login" onClick={() => {
|
<button key="start_login" onClick={() => {
|
||||||
modal.close();
|
modal.close();
|
||||||
dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
|
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
|
||||||
}}>{ _t('Sign In') }</button>,
|
}}>{ _t('Sign In') }</button>,
|
||||||
],
|
],
|
||||||
onFinished: (proceed) => {
|
onFinished: (proceed) => {
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
|
dis.dispatch({ action: 'start_registration', screenAfterLogin: options.screen_after });
|
||||||
} else if (options.go_home_on_cancel) {
|
} else if (options.go_home_on_cancel) {
|
||||||
dis.dispatch({action: 'view_home_page'});
|
dis.dispatch({ action: 'view_home_page' });
|
||||||
} else if (options.go_welcome_on_cancel) {
|
} else if (options.go_welcome_on_cancel) {
|
||||||
dis.dispatch({action: 'view_welcome_page'});
|
dis.dispatch({ action: 'view_welcome_page' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -31,6 +31,6 @@ export function textualPowerLevel(level: number, usersDefault: number): string {
|
||||||
if (LEVEL_ROLE_MAP[level]) {
|
if (LEVEL_ROLE_MAP[level]) {
|
||||||
return LEVEL_ROLE_MAP[level];
|
return LEVEL_ROLE_MAP[level];
|
||||||
} else {
|
} else {
|
||||||
return _t("Custom (%(level)s)", {level});
|
return _t("Custom (%(level)s)", { level });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2016 OpenMarket Ltd
|
Copyright 2016 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -16,15 +14,26 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
|
import MultiInviter, { CompletionStates } from './utils/MultiInviter';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './';
|
import * as sdk from './';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
|
import InviteDialog, { KIND_DM, KIND_INVITE, Member } from "./components/views/dialogs/InviteDialog";
|
||||||
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
|
||||||
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore";
|
||||||
|
import BaseAvatar from "./components/views/avatars/BaseAvatar";
|
||||||
|
import { mediaFromMxc } from "./customisations/Media";
|
||||||
|
|
||||||
|
export interface IInviteResult {
|
||||||
|
states: CompletionStates;
|
||||||
|
inviter: MultiInviter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room
|
* Invites multiple addresses to a room
|
||||||
|
@ -32,24 +41,24 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
|
||||||
* no option to cancel.
|
* no option to cancel.
|
||||||
*
|
*
|
||||||
* @param {string} roomId The ID of the room to invite to
|
* @param {string} roomId The ID of the room to invite to
|
||||||
* @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
* @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids.
|
||||||
* @returns {Promise} Promise
|
* @returns {Promise} Promise
|
||||||
*/
|
*/
|
||||||
export function inviteMultipleToRoom(roomId, addrs) {
|
export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise<IInviteResult> {
|
||||||
const inviter = new MultiInviter(roomId);
|
const inviter = new MultiInviter(roomId);
|
||||||
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog(initialText) {
|
export function showStartChatInviteDialog(initialText = ""): void {
|
||||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Start DM', '', InviteDialog, {kind: KIND_DM, initialText},
|
'Start DM', '', InviteDialog, { kind: KIND_DM, initialText },
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showRoomInviteDialog(roomId, initialText = "") {
|
export function showRoomInviteDialog(roomId: string, initialText = ""): void {
|
||||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
"Invite Users", "", InviteDialog, {
|
"Invite Users", "", InviteDialog, {
|
||||||
|
@ -61,14 +70,14 @@ export function showRoomInviteDialog(roomId, initialText = "") {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showCommunityRoomInviteDialog(roomId, communityName) {
|
export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void {
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId},
|
'Invite Users to Community', '', CommunityPrototypeInviteDialog, { communityName, roomId },
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showCommunityInviteDialog(communityId) {
|
export function showCommunityInviteDialog(communityId: string): void {
|
||||||
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId);
|
||||||
if (chat) {
|
if (chat) {
|
||||||
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
const name = CommunityPrototypeStore.instance.getCommunityName(communityId);
|
||||||
|
@ -83,7 +92,7 @@ export function showCommunityInviteDialog(communityId) {
|
||||||
* @param {MatrixEvent} event The event to check
|
* @param {MatrixEvent} event The event to check
|
||||||
* @returns {boolean} True if valid, false otherwise
|
* @returns {boolean} True if valid, false otherwise
|
||||||
*/
|
*/
|
||||||
export function isValid3pidInvite(event) {
|
export function isValid3pidInvite(event: MatrixEvent): boolean {
|
||||||
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
if (!event || event.getType() !== "m.room.third_party_invite") return false;
|
||||||
|
|
||||||
// any events without these keys are not valid 3pid invites, so we ignore them
|
// any events without these keys are not valid 3pid invites, so we ignore them
|
||||||
|
@ -96,7 +105,7 @@ export function isValid3pidInvite(event) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inviteUsersToRoom(roomId, userIds) {
|
export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise<void> {
|
||||||
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
return inviteMultipleToRoom(roomId, userIds).then((result) => {
|
||||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||||
showAnyInviteErrors(result.states, room, result.inviter);
|
showAnyInviteErrors(result.states, room, result.inviter);
|
||||||
|
@ -110,35 +119,68 @@ export function inviteUsersToRoom(roomId, userIds) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showAnyInviteErrors(addrs, room, inviter) {
|
export function showAnyInviteErrors(
|
||||||
|
states: CompletionStates,
|
||||||
|
room: Room,
|
||||||
|
inviter: MultiInviter,
|
||||||
|
userMap?: Map<string, Member>,
|
||||||
|
): boolean {
|
||||||
// Show user any errors
|
// Show user any errors
|
||||||
const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error');
|
const failedUsers = Object.keys(states).filter(a => states[a] === 'error');
|
||||||
if (failedUsers.length === 1 && inviter.fatal) {
|
if (failedUsers.length === 1 && inviter.fatal) {
|
||||||
// Just get the first message because there was a fatal problem on the first
|
// Just get the first message because there was a fatal problem on the first
|
||||||
// user. This usually means that no other users were attempted, making it
|
// user. This usually means that no other users were attempted, making it
|
||||||
// pointless for us to list who failed exactly.
|
// pointless for us to list who failed exactly.
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, {
|
Modal.createTrackedDialog('Failed to invite users to the room', '', ErrorDialog, {
|
||||||
title: _t("Failed to invite users to the room:", {roomName: room.name}),
|
title: _t("Failed to invite users to the room:", { roomName: room.name }),
|
||||||
description: inviter.getErrorText(failedUsers[0]),
|
description: inviter.getErrorText(failedUsers[0]),
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
const errorList = [];
|
const errorList = [];
|
||||||
for (const addr of failedUsers) {
|
for (const addr of failedUsers) {
|
||||||
if (addrs[addr] === "error") {
|
if (states[addr] === "error") {
|
||||||
const reason = inviter.getErrorText(addr);
|
const reason = inviter.getErrorText(addr);
|
||||||
errorList.push(addr + ": " + reason);
|
errorList.push(addr + ": " + reason);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
if (errorList.length > 0) {
|
if (errorList.length > 0) {
|
||||||
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
// React 16 doesn't let us use `errorList.join(<br />)` anymore, so this is our solution
|
||||||
const description = <div>{errorList.map(e => <div key={e}>{e}</div>)}</div>;
|
const description = <div className="mx_InviteDialog_multiInviterError">
|
||||||
|
<h4>{ _t("We sent the others, but the below people couldn't be invited to <RoomName/>", {}, {
|
||||||
|
RoomName: () => <b>{ room.name }</b>,
|
||||||
|
}) }</h4>
|
||||||
|
<div>
|
||||||
|
{ failedUsers.map(addr => {
|
||||||
|
const user = userMap?.get(addr) || cli.getUser(addr);
|
||||||
|
const name = (user as Member).name || (user as User).rawDisplayName;
|
||||||
|
const avatarUrl = (user as Member).getMxcAvatarUrl?.() || (user as User).avatarUrl;
|
||||||
|
return <div key={addr} className="mx_InviteDialog_multiInviterError_entry">
|
||||||
|
<div className="mx_InviteDialog_multiInviterError_entry_userProfile">
|
||||||
|
<BaseAvatar
|
||||||
|
url={avatarUrl ? mediaFromMxc(avatarUrl).getSquareThumbnailHttp(24) : null}
|
||||||
|
name={name}
|
||||||
|
idName={user.userId}
|
||||||
|
width={24}
|
||||||
|
height={24}
|
||||||
|
/>
|
||||||
|
<span className="mx_InviteDialog_multiInviterError_entry_name">{ name }</span>
|
||||||
|
<span className="mx_InviteDialog_multiInviterError_entry_userId">{ user.userId }</span>
|
||||||
|
</div>
|
||||||
|
<div className="mx_InviteDialog_multiInviterError_entry_error">
|
||||||
|
{ inviter.getErrorText(addr) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Failed to invite the following users to the room', '', ErrorDialog, {
|
Modal.createTrackedDialog("Some invites could not be sent", "", ErrorDialog, {
|
||||||
title: _t("Failed to invite the following users to the %(roomName)s room:", {roomName: room.name}),
|
title: _t("Some invites couldn't be sent"),
|
||||||
description,
|
description,
|
||||||
});
|
});
|
||||||
return false;
|
return false;
|
|
@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import {PushProcessor} from 'matrix-js-sdk/src/pushprocessor';
|
import { PushProcessor } from 'matrix-js-sdk/src/pushprocessor';
|
||||||
|
|
||||||
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
|
export const ALL_MESSAGES_LOUD = 'all_messages_loud';
|
||||||
export const ALL_MESSAGES = 'all_messages';
|
export const ALL_MESSAGES = 'all_messages';
|
||||||
|
@ -52,7 +52,7 @@ export function aggregateNotificationCount(rooms) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}, {count: 0, highlight: false});
|
}, { count: 0, highlight: false });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRoomHasBadge(room) {
|
export function getRoomHasBadge(room) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a room object, return the alias we should use for it,
|
* Given a room object, return the alias we should use for it,
|
||||||
|
@ -25,11 +27,11 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
* @param {Object} room The room object
|
* @param {Object} room The room object
|
||||||
* @returns {string} A display alias for the given room
|
* @returns {string} A display alias for the given room
|
||||||
*/
|
*/
|
||||||
export function getDisplayAliasForRoom(room) {
|
export function getDisplayAliasForRoom(room: Room): string {
|
||||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function looksLikeDirectMessageRoom(room, myUserId) {
|
export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {
|
||||||
const myMembership = room.getMyMembership();
|
const myMembership = room.getMyMembership();
|
||||||
const me = room.getMember(myUserId);
|
const me = room.getMember(myUserId);
|
||||||
|
|
||||||
|
@ -48,7 +50,7 @@ export function looksLikeDirectMessageRoom(room, myUserId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function guessAndSetDMRoom(room, isDirect) {
|
export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void> {
|
||||||
let newTarget;
|
let newTarget;
|
||||||
if (isDirect) {
|
if (isDirect) {
|
||||||
const guessedUserId = guessDMRoomTargetId(
|
const guessedUserId = guessDMRoomTargetId(
|
||||||
|
@ -70,7 +72,7 @@ export function guessAndSetDMRoom(room, isDirect) {
|
||||||
this room as a DM room
|
this room as a DM room
|
||||||
* @returns {object} A promise
|
* @returns {object} A promise
|
||||||
*/
|
*/
|
||||||
export function setDMRoom(roomId, userId) {
|
export function setDMRoom(roomId: string, userId: string): Promise<void> {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
@ -102,7 +104,6 @@ export function setDMRoom(roomId, userId) {
|
||||||
dmRoomMap[userId] = roomList;
|
dmRoomMap[userId] = roomList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
|
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,7 +115,7 @@ export function setDMRoom(roomId, userId) {
|
||||||
* @param {string} myUserId User ID of the current user
|
* @param {string} myUserId User ID of the current user
|
||||||
* @returns {string} User ID of the user that the room is probably a DM with
|
* @returns {string} User ID of the user that the room is probably a DM with
|
||||||
*/
|
*/
|
||||||
function guessDMRoomTargetId(room, myUserId) {
|
function guessDMRoomTargetId(room: Room, myUserId: string): string {
|
||||||
let oldestTs;
|
let oldestTs;
|
||||||
let oldestUser;
|
let oldestUser;
|
||||||
|
|
|
@ -17,12 +17,12 @@ limitations under the License.
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
|
import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import request from "browser-request";
|
import request from "browser-request";
|
||||||
|
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import {WidgetType} from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
// The version of the integration manager API we're intending to work with
|
// The version of the integration manager API we're intending to work with
|
||||||
|
@ -109,7 +109,7 @@ export default class ScalarAuthClient {
|
||||||
request({
|
request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
uri: url,
|
uri: url,
|
||||||
qs: {scalar_token: token, v: imApiVersion},
|
qs: { scalar_token: token, v: imApiVersion },
|
||||||
json: true,
|
json: true,
|
||||||
}, (err, response, body) => {
|
}, (err, response, body) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
@ -189,7 +189,7 @@ export default class ScalarAuthClient {
|
||||||
request({
|
request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
uri: scalarRestUrl + '/register',
|
uri: scalarRestUrl + '/register',
|
||||||
qs: {v: imApiVersion},
|
qs: { v: imApiVersion },
|
||||||
body: openidTokenObject,
|
body: openidTokenObject,
|
||||||
json: true,
|
json: true,
|
||||||
}, (err, response, body) => {
|
}, (err, response, body) => {
|
||||||
|
|
|
@ -208,7 +208,6 @@ Example:
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
membership_state AND bot_options
|
membership_state AND bot_options
|
||||||
--------------------------------
|
--------------------------------
|
||||||
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.
|
||||||
|
@ -236,15 +235,15 @@ Example:
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import RoomViewStore from './stores/RoomViewStore';
|
import RoomViewStore from './stores/RoomViewStore';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
import { IntegrationManagers } from "./integrations/IntegrationManagers";
|
||||||
import {WidgetType} from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import {objectClone} from "./utils/objects";
|
import { objectClone } from "./utils/objects";
|
||||||
|
|
||||||
function sendResponse(event, res) {
|
function sendResponse(event, res) {
|
||||||
const data = objectClone(event.data);
|
const data = objectClone(event.data);
|
||||||
|
@ -608,7 +607,7 @@ const onMessage = function(event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (roomId !== RoomViewStore.getRoomId()) {
|
if (roomId !== RoomViewStore.getRoomId()) {
|
||||||
sendError(event, _t('Room %(roomId)s not visible', {roomId: roomId}));
|
sendError(event, _t('Room %(roomId)s not visible', { roomId: roomId }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EventIndexPeg from "./indexing/EventIndexPeg";
|
import EventIndexPeg from "./indexing/EventIndexPeg";
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
|
|
||||||
const SEARCH_LIMIT = 10;
|
const SEARCH_LIMIT = 10;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ async function serverSideSearch(term, roomId = undefined) {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await client.search({body: body});
|
const response = await client.search({ body: body });
|
||||||
|
|
||||||
const result = {
|
const result = {
|
||||||
response: response,
|
response: response,
|
||||||
|
@ -498,7 +498,7 @@ async function combinedPagination(searchResult) {
|
||||||
// Fetch events from the server if we have a token for it and if it's the
|
// Fetch events from the server if we have a token for it and if it's the
|
||||||
// local indexes turn or the local index has exhausted its results.
|
// local indexes turn or the local index has exhausted its results.
|
||||||
if (searchResult.serverSideNextBatch && (oldestEventFrom === "local" || !searchArgs.next_batch)) {
|
if (searchResult.serverSideNextBatch && (oldestEventFrom === "local" || !searchArgs.next_batch)) {
|
||||||
const body = {body: searchResult._query, next_batch: searchResult.serverSideNextBatch};
|
const body = { body: searchResult._query, next_batch: searchResult.serverSideNextBatch };
|
||||||
serverSideResult = await client.search(body);
|
serverSideResult = await client.search(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
||||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces
|
||||||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import SecurityCustomisations from "./customisations/Security";
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning';
|
||||||
|
|
||||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||||
// only meant to act as a cache to avoid prompting the user multiple times
|
// only meant to act as a cache to avoid prompting the user multiple times
|
||||||
|
@ -41,8 +42,8 @@ let secretStorageBeingAccessed = false;
|
||||||
let nonInteractive = false;
|
let nonInteractive = false;
|
||||||
|
|
||||||
let dehydrationCache: {
|
let dehydrationCache: {
|
||||||
key?: Uint8Array,
|
key?: Uint8Array;
|
||||||
keyInfo?: ISecretStorageKeyInfo,
|
keyInfo?: ISecretStorageKeyInfo;
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
function isCachingAllowed(): boolean {
|
function isCachingAllowed(): boolean {
|
||||||
|
@ -134,7 +135,7 @@ async function getSecretStorageKey(
|
||||||
|
|
||||||
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||||
if (keyFromCustomisations) {
|
if (keyFromCustomisations) {
|
||||||
console.log("Using key from security customisations (secret storage)")
|
console.log("Using key from security customisations (secret storage)");
|
||||||
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
|
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
|
||||||
return [keyId, keyFromCustomisations];
|
return [keyId, keyFromCustomisations];
|
||||||
}
|
}
|
||||||
|
@ -184,7 +185,7 @@ export async function getDehydrationKey(
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||||
if (keyFromCustomisations) {
|
if (keyFromCustomisations) {
|
||||||
console.log("Using key from security customisations (dehydration)")
|
console.log("Using key from security customisations (dehydration)");
|
||||||
return keyFromCustomisations;
|
return keyFromCustomisations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +224,7 @@ export async function getDehydrationKey(
|
||||||
const key = await inputToKey(input);
|
const key = await inputToKey(input);
|
||||||
|
|
||||||
// need to copy the key because rehydration (unpickling) will clobber it
|
// need to copy the key because rehydration (unpickling) will clobber it
|
||||||
dehydrationCache = {key: new Uint8Array(key), keyInfo};
|
dehydrationCache = { key: new Uint8Array(key), keyInfo };
|
||||||
|
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
@ -244,7 +245,7 @@ async function onSecretRequested(
|
||||||
deviceId: string,
|
deviceId: string,
|
||||||
requestId: string,
|
requestId: string,
|
||||||
name: string,
|
name: string,
|
||||||
deviceTrust: IDeviceTrustLevel,
|
deviceTrust: DeviceTrustLevel,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
//@flow
|
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Aviral Dasgupta
|
Copyright 2017 Aviral Dasgupta
|
||||||
|
|
||||||
|
@ -15,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {clamp} from "lodash";
|
import { clamp } from "lodash";
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
import {SerializedPart} from "./editor/parts";
|
import { SerializedPart } from "./editor/parts";
|
||||||
import EditorModel from "./editor/model";
|
import EditorModel from "./editor/model";
|
||||||
|
|
||||||
interface IHistoryItem {
|
interface IHistoryItem {
|
||||||
|
|
|
@ -17,25 +17,25 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { User } from "matrix-js-sdk/src/models/user";
|
||||||
|
|
||||||
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
|
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import * as sdk from './index';
|
import * as sdk from './index';
|
||||||
import {_t, _td} from './languageHandler';
|
import { _t, _td } from './languageHandler';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import MultiInviter from './utils/MultiInviter';
|
import MultiInviter from './utils/MultiInviter';
|
||||||
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
import { linkifyAndSanitizeHtml } from './HtmlUtils';
|
||||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||||
import WidgetUtils from "./utils/WidgetUtils";
|
import WidgetUtils from "./utils/WidgetUtils";
|
||||||
import {textToHtmlRainbow} from "./utils/colour";
|
import { textToHtmlRainbow } from "./utils/colour";
|
||||||
import { getAddressType } from './UserAddress';
|
import { getAddressType } from './UserAddress';
|
||||||
import { abbreviateUrl } from './utils/UrlUtils';
|
import { abbreviateUrl } from './utils/UrlUtils';
|
||||||
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
|
||||||
import {isPermalinkHost, parsePermalink} from "./utils/permalinks/Permalinks";
|
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
|
||||||
import {inviteUsersToRoom} from "./RoomInvite";
|
import { inviteUsersToRoom } from "./RoomInvite";
|
||||||
import { WidgetType } from "./widgets/WidgetType";
|
import { WidgetType } from "./widgets/WidgetType";
|
||||||
import { Jitsi } from "./widgets/Jitsi";
|
import { Jitsi } from "./widgets/Jitsi";
|
||||||
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
|
||||||
|
@ -46,10 +46,10 @@ import { Action } from "./dispatcher/actions";
|
||||||
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
|
import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from "./utils/membership";
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import { UIFeature } from "./settings/UIFeature";
|
||||||
import {CHAT_EFFECTS} from "./effects"
|
import { CHAT_EFFECTS } from "./effects";
|
||||||
import CallHandler from "./CallHandler";
|
import CallHandler from "./CallHandler";
|
||||||
import {guessAndSetDMRoom} from "./Rooms";
|
import { guessAndSetDMRoom } from "./Rooms";
|
||||||
|
|
||||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||||
interface HTMLInputEvent extends Event {
|
interface HTMLInputEvent extends Event {
|
||||||
|
@ -143,11 +143,11 @@ export class Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
function reject(error) {
|
function reject(error) {
|
||||||
return {error};
|
return { error };
|
||||||
}
|
}
|
||||||
|
|
||||||
function success(promise?: Promise<any>) {
|
function success(promise?: Promise<any>) {
|
||||||
return {promise};
|
return { promise };
|
||||||
}
|
}
|
||||||
|
|
||||||
function successSync(value: any) {
|
function successSync(value: any) {
|
||||||
|
@ -271,8 +271,8 @@ export const Commands = [
|
||||||
|
|
||||||
const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog");
|
const RoomUpgradeWarningDialog = sdk.getComponent("dialogs.RoomUpgradeWarningDialog");
|
||||||
|
|
||||||
const {finished} = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
|
const { finished } = Modal.createTrackedDialog('Slash Commands', 'upgrade room confirmation',
|
||||||
RoomUpgradeWarningDialog, {roomId: roomId, targetVersion: args}, /*className=*/null,
|
RoomUpgradeWarningDialog, { roomId: roomId, targetVersion: args }, /*className=*/null,
|
||||||
/*isPriority=*/false, /*isStatic=*/true);
|
/*isPriority=*/false, /*isStatic=*/true);
|
||||||
|
|
||||||
return success(finished.then(async ([resp]) => {
|
return success(finished.then(async ([resp]) => {
|
||||||
|
@ -288,7 +288,7 @@ export const Commands = [
|
||||||
if (resp.invite) {
|
if (resp.invite) {
|
||||||
checkForUpgradeFn = async (newRoom) => {
|
checkForUpgradeFn = async (newRoom) => {
|
||||||
// The upgradePromise should be done by the time we await it here.
|
// The upgradePromise should be done by the time we await it here.
|
||||||
const {replacement_room: newRoomId} = await upgradePromise;
|
const { replacement_room: newRoomId } = await upgradePromise;
|
||||||
if (newRoom.roomId !== newRoomId) return;
|
if (newRoom.roomId !== newRoomId) return;
|
||||||
|
|
||||||
const toInvite = [
|
const toInvite = [
|
||||||
|
@ -370,7 +370,7 @@ export const Commands = [
|
||||||
|
|
||||||
return success(promise.then((url) => {
|
return success(promise.then((url) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.avatar', {url}, '');
|
return MatrixClientPeg.get().sendStateEvent(roomId, 'm.room.avatar', { url }, '');
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
category: CommandCategories.actions,
|
category: CommandCategories.actions,
|
||||||
|
@ -741,7 +741,7 @@ export const Commands = [
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'User ignored', InfoDialog, {
|
||||||
title: _t('Ignored user'),
|
title: _t('Ignored user'),
|
||||||
description: <div>
|
description: <div>
|
||||||
<p>{ _t('You are now ignoring %(userId)s', {userId}) }</p>
|
<p>{ _t('You are now ignoring %(userId)s', { userId }) }</p>
|
||||||
</div>,
|
</div>,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -772,7 +772,7 @@ export const Commands = [
|
||||||
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
|
Modal.createTrackedDialog('Slash Commands', 'User unignored', InfoDialog, {
|
||||||
title: _t('Unignored user'),
|
title: _t('Unignored user'),
|
||||||
description: <div>
|
description: <div>
|
||||||
<p>{ _t('You are no longer ignoring %(userId)s', {userId}) }</p>
|
<p>{ _t('You are no longer ignoring %(userId)s', { userId }) }</p>
|
||||||
</div>,
|
</div>,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@ -839,7 +839,7 @@ export const Commands = [
|
||||||
description: _td('Opens the Developer Tools dialog'),
|
description: _td('Opens the Developer Tools dialog'),
|
||||||
runFn: function(roomId) {
|
runFn: function(roomId) {
|
||||||
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
const DevtoolsDialog = sdk.getComponent('dialogs.DevtoolsDialog');
|
||||||
Modal.createDialog(DevtoolsDialog, {roomId});
|
Modal.createDialog(DevtoolsDialog, { roomId });
|
||||||
return success();
|
return success();
|
||||||
},
|
},
|
||||||
category: CommandCategories.advanced,
|
category: CommandCategories.advanced,
|
||||||
|
@ -951,7 +951,7 @@ export const Commands = [
|
||||||
{
|
{
|
||||||
_t('The signing key you provided matches the signing key you received ' +
|
_t('The signing key you provided matches the signing key you received ' +
|
||||||
'from %(userId)s\'s session %(deviceId)s. Session marked as verified.',
|
'from %(userId)s\'s session %(deviceId)s. Session marked as verified.',
|
||||||
{userId, deviceId})
|
{ userId, deviceId })
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>,
|
</div>,
|
||||||
|
@ -1019,9 +1019,8 @@ export const Commands = [
|
||||||
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
||||||
dis.dispatch<ViewUserPayload>({
|
dis.dispatch<ViewUserPayload>({
|
||||||
action: Action.ViewUser,
|
action: Action.ViewUser,
|
||||||
// XXX: We should be using a real member object and not assuming what the
|
// XXX: We should be using a real member object and not assuming what the receiver wants.
|
||||||
// receiver wants.
|
member: member || { userId } as User,
|
||||||
member: member || {userId},
|
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
},
|
},
|
||||||
|
@ -1173,16 +1172,16 @@ export const Commands = [
|
||||||
};
|
};
|
||||||
MatrixClientPeg.get().sendMessage(roomId, content);
|
MatrixClientPeg.get().sendMessage(roomId, content);
|
||||||
}
|
}
|
||||||
dis.dispatch({action: `effects.${effect.command}`});
|
dis.dispatch({ action: `effects.${effect.command}` });
|
||||||
})());
|
})());
|
||||||
},
|
},
|
||||||
category: CommandCategories.effects,
|
category: CommandCategories.effects,
|
||||||
})
|
});
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// build a map from names and aliases to the Command objects.
|
// build a map from names and aliases to the Command objects.
|
||||||
export const CommandMap = new Map();
|
export const CommandMap = new Map<string, Command>();
|
||||||
Commands.forEach(cmd => {
|
Commands.forEach(cmd => {
|
||||||
CommandMap.set(cmd.command, cmd);
|
CommandMap.set(cmd.command, cmd);
|
||||||
cmd.aliases.forEach(alias => {
|
cmd.aliases.forEach(alias => {
|
||||||
|
@ -1190,15 +1189,15 @@ Commands.forEach(cmd => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export function parseCommandString(input: string) {
|
export function parseCommandString(input: string): { cmd?: string, args?: string } {
|
||||||
// trim any trailing whitespace, as it can confuse the parser for
|
// trim any trailing whitespace, as it can confuse the parser for
|
||||||
// IRC-style commands
|
// IRC-style commands
|
||||||
input = input.replace(/\s+$/, '');
|
input = input.replace(/\s+$/, '');
|
||||||
if (input[0] !== '/') return {}; // not a command
|
if (input[0] !== '/') return {}; // not a command
|
||||||
|
|
||||||
const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
|
const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
|
||||||
let cmd;
|
let cmd: string;
|
||||||
let args;
|
let args: string;
|
||||||
if (bits) {
|
if (bits) {
|
||||||
cmd = bits[1].substring(1).toLowerCase();
|
cmd = bits[1].substring(1).toLowerCase();
|
||||||
args = bits[2];
|
args = bits[2];
|
||||||
|
@ -1206,7 +1205,12 @@ export function parseCommandString(input: string) {
|
||||||
cmd = input;
|
cmd = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {cmd, args};
|
return { cmd, args };
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ICmd {
|
||||||
|
cmd?: Command;
|
||||||
|
args?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1217,8 +1221,8 @@ export function parseCommandString(input: string) {
|
||||||
* processing the command, or 'promise' if a request was sent out.
|
* processing the command, or 'promise' if a request was sent out.
|
||||||
* Returns null if the input didn't match a command.
|
* Returns null if the input didn't match a command.
|
||||||
*/
|
*/
|
||||||
export function getCommand(input: string) {
|
export function getCommand(input: string): ICmd {
|
||||||
const {cmd, args} = parseCommandString(input);
|
const { cmd, args } = parseCommandString(input);
|
||||||
|
|
||||||
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
|
if (CommandMap.has(cmd) && CommandMap.get(cmd).isEnabled()) {
|
||||||
return {
|
return {
|
||||||
|
|
21
src/Terms.ts
|
@ -15,8 +15,9 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { SERVICE_TYPES } from 'matrix-js-sdk/src/service-types';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import * as sdk from '.';
|
import * as sdk from '.';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ export class Service {
|
||||||
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
|
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
|
||||||
* @param {string} accessToken The user's access token for the service
|
* @param {string} accessToken The user's access token for the service
|
||||||
*/
|
*/
|
||||||
constructor(public serviceType: string, public baseUrl: string, public accessToken: string) {
|
constructor(public serviceType: SERVICE_TYPES, public baseUrl: string, public accessToken: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,13 +49,13 @@ export interface Policy {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Policies = {
|
export type Policies = {
|
||||||
[policy: string]: Policy,
|
[policy: string]: Policy;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TermsInteractionCallback = (
|
export type TermsInteractionCallback = (
|
||||||
policiesAndServicePairs: {
|
policiesAndServicePairs: {
|
||||||
service: Service,
|
service: Service;
|
||||||
policies: Policies,
|
policies: Policies;
|
||||||
}[],
|
}[],
|
||||||
agreedUrls: string[],
|
agreedUrls: string[],
|
||||||
extraClassNames?: string,
|
extraClassNames?: string,
|
||||||
|
@ -117,7 +118,7 @@ export async function startTermsFlow(
|
||||||
// but that is not a thing the API supports, so probably best to just show
|
// but that is not a thing the API supports, so probably best to just show
|
||||||
// things they've not agreed to yet.
|
// things they've not agreed to yet.
|
||||||
const unagreedPoliciesAndServicePairs = [];
|
const unagreedPoliciesAndServicePairs = [];
|
||||||
for (const {service, policies} of policiesAndServicePairs) {
|
for (const { service, policies } of policiesAndServicePairs) {
|
||||||
const unagreedPolicies = {};
|
const unagreedPolicies = {};
|
||||||
for (const [policyName, policy] of Object.entries(policies)) {
|
for (const [policyName, policy] of Object.entries(policies)) {
|
||||||
let policyAgreed = false;
|
let policyAgreed = false;
|
||||||
|
@ -131,7 +132,7 @@ export async function startTermsFlow(
|
||||||
if (!policyAgreed) unagreedPolicies[policyName] = policy;
|
if (!policyAgreed) unagreedPolicies[policyName] = policy;
|
||||||
}
|
}
|
||||||
if (Object.keys(unagreedPolicies).length > 0) {
|
if (Object.keys(unagreedPolicies).length > 0) {
|
||||||
unagreedPoliciesAndServicePairs.push({service, policies: unagreedPolicies});
|
unagreedPoliciesAndServicePairs.push({ service, policies: unagreedPolicies });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,7 +149,7 @@ export async function startTermsFlow(
|
||||||
|
|
||||||
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length
|
// We only ever add to the set of URLs, so if anything has changed then we'd see a different length
|
||||||
if (agreedUrlSet.size !== numAcceptedBeforeAgreement) {
|
if (agreedUrlSet.size !== numAcceptedBeforeAgreement) {
|
||||||
const newAcceptedTerms = {accepted: Array.from(agreedUrlSet)};
|
const newAcceptedTerms = { accepted: Array.from(agreedUrlSet) };
|
||||||
await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms);
|
await MatrixClientPeg.get().setAccountData('m.accepted_terms', newAcceptedTerms);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,8 +181,8 @@ export async function startTermsFlow(
|
||||||
|
|
||||||
export function dialogTermsInteractionCallback(
|
export function dialogTermsInteractionCallback(
|
||||||
policiesAndServicePairs: {
|
policiesAndServicePairs: {
|
||||||
service: Service,
|
service: Service;
|
||||||
policies: { [policy: string]: Policy },
|
policies: { [policy: string]: Policy };
|
||||||
}[],
|
}[],
|
||||||
agreedUrls: string[],
|
agreedUrls: string[],
|
||||||
extraClassNames?: string,
|
extraClassNames?: string,
|
||||||
|
|
|
@ -13,13 +13,20 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
|
||||||
|
import React from 'react';
|
||||||
|
import { MatrixClientPeg } from './MatrixClientPeg';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
import {isValid3pidInvite} from "./RoomInvite";
|
import { isValid3pidInvite } from "./RoomInvite";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
|
import { ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES } from "./mjolnir/BanList";
|
||||||
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
|
import { WIDGET_LAYOUT_EVENT_TYPE } from "./stores/widgets/WidgetLayoutStore";
|
||||||
|
import { RightPanelPhases } from './stores/RightPanelStorePhases';
|
||||||
|
import { Action } from './dispatcher/actions';
|
||||||
|
import defaultDispatcher from './dispatcher/dispatcher';
|
||||||
|
import { SetRightPanelPhasePayload } from './dispatcher/payloads/SetRightPanelPhasePayload';
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
// These functions are frequently used just to check whether an event has
|
// These functions are frequently used just to check whether an event has
|
||||||
// any text to display at all. For this reason they return deferred values
|
// any text to display at all. For this reason they return deferred values
|
||||||
|
@ -31,76 +38,89 @@ function textForMemberEvent(ev): () => string | null {
|
||||||
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
const targetName = ev.target ? ev.target.name : ev.getStateKey();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const content = ev.getContent();
|
const content = ev.getContent();
|
||||||
|
const reason = content.reason;
|
||||||
|
|
||||||
const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : '';
|
|
||||||
switch (content.membership) {
|
switch (content.membership) {
|
||||||
case 'invite': {
|
case 'invite': {
|
||||||
const threePidContent = content.third_party_invite;
|
const threePidContent = content.third_party_invite;
|
||||||
if (threePidContent) {
|
if (threePidContent) {
|
||||||
if (threePidContent.display_name) {
|
if (threePidContent.display_name) {
|
||||||
return () => _t('%(targetName)s accepted the invitation for %(displayName)s.', {
|
return () => _t('%(targetName)s accepted the invitation for %(displayName)s', {
|
||||||
targetName,
|
targetName,
|
||||||
displayName: threePidContent.display_name,
|
displayName: threePidContent.display_name,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(targetName)s accepted an invitation.', {targetName});
|
return () => _t('%(targetName)s accepted an invitation', { targetName });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s invited %(targetName)s', { senderName, targetName });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'ban':
|
case 'ban':
|
||||||
return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
return () => reason
|
||||||
|
? _t('%(senderName)s banned %(targetName)s: %(reason)s', { senderName, targetName, reason })
|
||||||
|
: _t('%(senderName)s banned %(targetName)s', { senderName, targetName });
|
||||||
case 'join':
|
case 'join':
|
||||||
if (prevContent && prevContent.membership === 'join') {
|
if (prevContent && prevContent.membership === 'join') {
|
||||||
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) {
|
||||||
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s.', {
|
return () => _t('%(oldDisplayName)s changed their display name to %(displayName)s', {
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (!prevContent.displayname && content.displayname) {
|
} else if (!prevContent.displayname && content.displayname) {
|
||||||
return () => _t('%(senderName)s set their display name to %(displayName)s.', {
|
return () => _t('%(senderName)s set their display name to %(displayName)s', {
|
||||||
senderName: ev.getSender(),
|
senderName: ev.getSender(),
|
||||||
displayName: content.displayname,
|
displayName: content.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.displayname && !content.displayname) {
|
} else if (prevContent.displayname && !content.displayname) {
|
||||||
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s).', {
|
return () => _t('%(senderName)s removed their display name (%(oldDisplayName)s)', {
|
||||||
senderName,
|
senderName,
|
||||||
oldDisplayName: prevContent.displayname,
|
oldDisplayName: prevContent.displayname,
|
||||||
});
|
});
|
||||||
} else if (prevContent.avatar_url && !content.avatar_url) {
|
} else if (prevContent.avatar_url && !content.avatar_url) {
|
||||||
return () => _t('%(senderName)s removed their profile picture.', {senderName});
|
return () => _t('%(senderName)s removed their profile picture', { senderName });
|
||||||
} else if (prevContent.avatar_url && content.avatar_url &&
|
} else if (prevContent.avatar_url && content.avatar_url &&
|
||||||
prevContent.avatar_url !== content.avatar_url) {
|
prevContent.avatar_url !== content.avatar_url) {
|
||||||
return () => _t('%(senderName)s changed their profile picture.', {senderName});
|
return () => _t('%(senderName)s changed their profile picture', { senderName });
|
||||||
} else if (!prevContent.avatar_url && content.avatar_url) {
|
} else if (!prevContent.avatar_url && content.avatar_url) {
|
||||||
return () => _t('%(senderName)s set a profile picture.', {senderName});
|
return () => _t('%(senderName)s set a profile picture', { senderName });
|
||||||
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
} else if (SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||||
// This is a null rejoin, it will only be visible if the Labs option is enabled
|
// This is a null rejoin, it will only be visible if using 'show hidden events' (labs)
|
||||||
return () => _t("%(senderName)s made no change.", {senderName});
|
return () => _t("%(senderName)s made no change", { senderName });
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key);
|
||||||
return () => _t('%(targetName)s joined the room.', {targetName});
|
return () => _t('%(targetName)s joined the room', { targetName });
|
||||||
}
|
}
|
||||||
case 'leave':
|
case 'leave':
|
||||||
if (ev.getSender() === ev.getStateKey()) {
|
if (ev.getSender() === ev.getStateKey()) {
|
||||||
if (prevContent.membership === "invite") {
|
if (prevContent.membership === "invite") {
|
||||||
return () => _t('%(targetName)s rejected the invitation.', {targetName});
|
return () => _t('%(targetName)s rejected the invitation', { targetName });
|
||||||
} else {
|
} else {
|
||||||
return () => _t('%(targetName)s left the room.', {targetName});
|
return () => reason
|
||||||
|
? _t('%(targetName)s left the room: %(reason)s', { targetName, reason })
|
||||||
|
: _t('%(targetName)s left the room', { targetName });
|
||||||
}
|
}
|
||||||
} else if (prevContent.membership === "ban") {
|
} else if (prevContent.membership === "ban") {
|
||||||
return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName});
|
return () => _t('%(senderName)s unbanned %(targetName)s', { senderName, targetName });
|
||||||
} else if (prevContent.membership === "invite") {
|
} else if (prevContent.membership === "invite") {
|
||||||
return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', {
|
return () => reason
|
||||||
senderName,
|
? _t('%(senderName)s withdrew %(targetName)s\'s invitation: %(reason)s', {
|
||||||
targetName,
|
senderName,
|
||||||
}) + ' ' + getReason();
|
targetName,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName });
|
||||||
} else if (prevContent.membership === "join") {
|
} else if (prevContent.membership === "join") {
|
||||||
return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason();
|
return () => reason
|
||||||
|
? _t('%(senderName)s kicked %(targetName)s: %(reason)s', {
|
||||||
|
senderName,
|
||||||
|
targetName,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
|
: _t('%(senderName)s kicked %(targetName)s', { senderName, targetName });
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -119,7 +139,7 @@ function textForRoomNameEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
|
||||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||||
return () => _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s removed the room name.', { senderDisplayName });
|
||||||
}
|
}
|
||||||
if (ev.getPrevContent().name) {
|
if (ev.getPrevContent().name) {
|
||||||
return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
return () => _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||||
|
@ -136,7 +156,7 @@ function textForRoomNameEvent(ev): () => string | null {
|
||||||
|
|
||||||
function textForTombstoneEvent(ev): () => string | null {
|
function textForTombstoneEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
return () => _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s upgraded this room.', { senderDisplayName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForJoinRulesEvent(ev): () => string | null {
|
function textForJoinRulesEvent(ev): () => string | null {
|
||||||
|
@ -163,9 +183,9 @@ function textForGuestAccessEvent(ev): () => string | null {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
switch (ev.getContent().guest_access) {
|
switch (ev.getContent().guest_access) {
|
||||||
case "can_join":
|
case "can_join":
|
||||||
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s has allowed guests to join the room.', { senderDisplayName });
|
||||||
case "forbidden":
|
case "forbidden":
|
||||||
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', {senderDisplayName});
|
return () => _t('%(senderDisplayName)s has prevented guests from joining the room.', { senderDisplayName });
|
||||||
default:
|
default:
|
||||||
// There's no other options we can expect, however just for safety's sake we'll do this.
|
// There's no other options we can expect, however just for safety's sake we'll do this.
|
||||||
return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
return () => _t('%(senderDisplayName)s changed guest access to %(rule)s', {
|
||||||
|
@ -217,9 +237,9 @@ function textForServerACLEvent(ev): () => string | null {
|
||||||
|
|
||||||
let getText = null;
|
let getText = null;
|
||||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||||
getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
getText = () => _t("%(senderDisplayName)s set the server ACLs for this room.", { senderDisplayName });
|
||||||
} else {
|
} else {
|
||||||
getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
getText = () => _t("%(senderDisplayName)s changed the server ACLs for this room.", { senderDisplayName });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(current.allow)) {
|
if (!Array.isArray(current.allow)) {
|
||||||
|
@ -242,7 +262,7 @@ function textForMessageEvent(ev): () => string | null {
|
||||||
if (ev.getContent().msgtype === "m.emote") {
|
if (ev.getContent().msgtype === "m.emote") {
|
||||||
message = "* " + senderDisplayName + " " + message;
|
message = "* " + senderDisplayName + " " + message;
|
||||||
} else if (ev.getContent().msgtype === "m.image") {
|
} else if (ev.getContent().msgtype === "m.image") {
|
||||||
message = _t('%(senderDisplayName)s sent an image.', {senderDisplayName});
|
message = _t('%(senderDisplayName)s sent an image.', { senderDisplayName });
|
||||||
}
|
}
|
||||||
return message;
|
return message;
|
||||||
};
|
};
|
||||||
|
@ -303,7 +323,7 @@ function textForCallAnswerEvent(event): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
|
||||||
return _t('%(senderName)s answered the call.', {senderName}) + ' ' + supported;
|
return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,16 +358,16 @@ function textForCallHangupEvent(event): () => string | null {
|
||||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
getReason = () => '';
|
getReason = () => '';
|
||||||
} else {
|
} else {
|
||||||
getReason = () => _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return () => _t('%(senderName)s ended the call.', {senderName: getSenderName()}) + ' ' + getReason();
|
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForCallRejectEvent(event): () => string | null {
|
function textForCallRejectEvent(event): () => string | null {
|
||||||
return () => {
|
return () => {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
return _t('%(senderName)s declined the call.', {senderName});
|
return _t('%(senderName)s declined the call.', { senderName });
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,14 +424,14 @@ function textForHistoryVisibilityEvent(event): () => string | null {
|
||||||
switch (event.getContent().history_visibility) {
|
switch (event.getContent().history_visibility) {
|
||||||
case 'invited':
|
case 'invited':
|
||||||
return () => _t('%(senderName)s made future room history visible to all room members, '
|
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||||
+ 'from the point they are invited.', {senderName});
|
+ 'from the point they are invited.', { senderName });
|
||||||
case 'joined':
|
case 'joined':
|
||||||
return () => _t('%(senderName)s made future room history visible to all room members, '
|
return () => _t('%(senderName)s made future room history visible to all room members, '
|
||||||
+ 'from the point they joined.', {senderName});
|
+ 'from the point they joined.', { senderName });
|
||||||
case 'shared':
|
case 'shared':
|
||||||
return () => _t('%(senderName)s made future room history visible to all room members.', {senderName});
|
return () => _t('%(senderName)s made future room history visible to all room members.', { senderName });
|
||||||
case 'world_readable':
|
case 'world_readable':
|
||||||
return () => _t('%(senderName)s made future room history visible to anyone.', {senderName});
|
return () => _t('%(senderName)s made future room history visible to anyone.', { senderName });
|
||||||
default:
|
default:
|
||||||
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
return () => _t('%(senderName)s made future room history visible to unknown (%(visibility)s).', {
|
||||||
senderName,
|
senderName,
|
||||||
|
@ -466,15 +486,39 @@ function textForPowerEvent(event): () => string | null {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForPinnedEvent(event): () => string | null {
|
const onPinnedMessagesClick = (): void => {
|
||||||
|
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
|
||||||
|
action: Action.SetRightPanelPhase,
|
||||||
|
phase: RightPanelPhases.PinnedMessages,
|
||||||
|
allowClose: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
|
||||||
|
if (!SettingsStore.getValue("feature_pinning")) return null;
|
||||||
const senderName = event.sender ? event.sender.name : event.getSender();
|
const senderName = event.sender ? event.sender.name : event.getSender();
|
||||||
return () => _t("%(senderName)s changed the pinned messages for the room.", {senderName});
|
|
||||||
|
if (allowJSX) {
|
||||||
|
return () => (
|
||||||
|
<span>
|
||||||
|
{
|
||||||
|
_t(
|
||||||
|
"%(senderName)s changed the <a>pinned messages</a> for the room.",
|
||||||
|
{ senderName },
|
||||||
|
{ "a": (sub) => <a onClick={onPinnedMessagesClick}> { sub } </a> },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => _t("%(senderName)s changed the pinned messages for the room.", { senderName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForWidgetEvent(event): () => string | null {
|
function textForWidgetEvent(event): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const {name: prevName, type: prevType, url: prevUrl} = event.getPrevContent();
|
const { name: prevName, type: prevType, url: prevUrl } = event.getPrevContent();
|
||||||
const {name, type, url} = event.getContent() || {};
|
const { name, type, url } = event.getContent() || {};
|
||||||
|
|
||||||
let widgetName = name || prevName || type || prevType || '';
|
let widgetName = name || prevName || type || prevType || '';
|
||||||
// Apply sentence case to widget name
|
// Apply sentence case to widget name
|
||||||
|
@ -503,68 +547,68 @@ function textForWidgetEvent(event): () => string | null {
|
||||||
|
|
||||||
function textForWidgetLayoutEvent(event): () => string | null {
|
function textForWidgetLayoutEvent(event): () => string | null {
|
||||||
const senderName = event.sender?.name || event.getSender();
|
const senderName = event.sender?.name || event.getSender();
|
||||||
return () => _t("%(senderName)s has updated the widget layout", {senderName});
|
return () => _t("%(senderName)s has updated the widget layout", { senderName });
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMjolnirEvent(event): () => string | null {
|
function textForMjolnirEvent(event): () => string | null {
|
||||||
const senderName = event.getSender();
|
const senderName = event.getSender();
|
||||||
const {entity: prevEntity} = event.getPrevContent();
|
const { entity: prevEntity } = event.getPrevContent();
|
||||||
const {entity, recommendation, reason} = event.getContent();
|
const { entity, recommendation, reason } = event.getContent();
|
||||||
|
|
||||||
// Rule removed
|
// Rule removed
|
||||||
if (!entity) {
|
if (!entity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning users matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{ senderName, glob: prevEntity });
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning rooms matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{ senderName, glob: prevEntity });
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
return () => _t("%(senderName)s removed the rule banning servers matching %(glob)s",
|
||||||
{senderName, glob: prevEntity});
|
{ senderName, glob: prevEntity });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something, but we shouldn't end up here.
|
// Unknown type. We'll say something, but we shouldn't end up here.
|
||||||
return () => _t("%(senderName)s removed a ban rule matching %(glob)s", {senderName, glob: prevEntity});
|
return () => _t("%(senderName)s removed a ban rule matching %(glob)s", { senderName, glob: prevEntity });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid rule
|
// Invalid rule
|
||||||
if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, {senderName});
|
if (!recommendation || !reason) return () => _t(`%(senderName)s updated an invalid ban rule`, { senderName });
|
||||||
|
|
||||||
// Rule updated
|
// Rule updated
|
||||||
if (entity === prevEntity) {
|
if (entity === prevEntity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning users matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning rooms matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated the rule banning servers matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s updated a ban rule matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
// New rule
|
// New rule
|
||||||
if (!prevEntity) {
|
if (!prevEntity) {
|
||||||
if (USER_RULE_TYPES.includes(event.getType())) {
|
if (USER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning users matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning rooms matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a rule banning servers matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
return () => _t("%(senderName)s created a ban rule matching %(glob)s for %(reason)s",
|
||||||
{senderName, glob: entity, reason});
|
{ senderName, glob: entity, reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
// else the entity !== prevEntity - count as a removal & add
|
// else the entity !== prevEntity - count as a removal & add
|
||||||
|
@ -572,29 +616,29 @@ function textForMjolnirEvent(event): () => string | null {
|
||||||
return () => _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning users matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
|
||||||
);
|
);
|
||||||
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
} else if (ROOM_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning rooms matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
|
||||||
);
|
);
|
||||||
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
} else if (SERVER_RULE_TYPES.includes(event.getType())) {
|
||||||
return () => _t(
|
return () => _t(
|
||||||
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
"%(senderName)s changed a rule that was banning servers matching %(oldGlob)s to matching " +
|
||||||
"%(newGlob)s for %(reason)s",
|
"%(newGlob)s for %(reason)s",
|
||||||
{senderName, oldGlob: prevEntity, newGlob: entity, reason},
|
{ senderName, oldGlob: prevEntity, newGlob: entity, reason },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown type. We'll say something but we shouldn't end up here.
|
// Unknown type. We'll say something but we shouldn't end up here.
|
||||||
return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
return () => _t("%(senderName)s updated a ban rule that was matching %(oldGlob)s to matching %(newGlob)s " +
|
||||||
"for %(reason)s", {senderName, oldGlob: prevEntity, newGlob: entity, reason});
|
"for %(reason)s", { senderName, oldGlob: prevEntity, newGlob: entity, reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHandlers {
|
interface IHandlers {
|
||||||
[type: string]: (ev: any) => (() => string | null);
|
[type: string]: (ev: MatrixEvent, allowJSX?: boolean) => (() => string | JSX.Element | null);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlers: IHandlers = {
|
const handlers: IHandlers = {
|
||||||
|
@ -635,7 +679,9 @@ export function hasText(ev): boolean {
|
||||||
return Boolean(handler?.(ev));
|
return Boolean(handler?.(ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textForEvent(ev): string {
|
export function textForEvent(ev: MatrixEvent): string;
|
||||||
|
export function textForEvent(ev: MatrixEvent, allowJSX: true): string | JSX.Element;
|
||||||
|
export function textForEvent(ev: MatrixEvent, allowJSX = false): string | JSX.Element {
|
||||||
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
const handler = (ev.isState() ? stateHandlers : handlers)[ev.getType()];
|
||||||
return handler?.(ev)?.() || '';
|
return handler?.(ev, allowJSX)?.() || '';
|
||||||
}
|
}
|
458
src/Tinter.js
|
@ -1,458 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2015 OpenMarket Ltd
|
|
||||||
Copyright 2017 New Vector Ltd
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const DEBUG = 0;
|
|
||||||
|
|
||||||
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
|
|
||||||
function colorToRgb(color) {
|
|
||||||
if (!color) {
|
|
||||||
return [0, 0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (color[0] === '#') {
|
|
||||||
color = color.slice(1);
|
|
||||||
if (color.length === 3) {
|
|
||||||
color = color[0] + color[0] +
|
|
||||||
color[1] + color[1] +
|
|
||||||
color[2] + color[2];
|
|
||||||
}
|
|
||||||
const val = parseInt(color, 16);
|
|
||||||
const r = (val >> 16) & 255;
|
|
||||||
const g = (val >> 8) & 255;
|
|
||||||
const b = val & 255;
|
|
||||||
return [r, g, b];
|
|
||||||
} else {
|
|
||||||
const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
|
|
||||||
if (match) {
|
|
||||||
return [
|
|
||||||
parseInt(match[1]),
|
|
||||||
parseInt(match[2]),
|
|
||||||
parseInt(match[3]),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [0, 0, 0];
|
|
||||||
}
|
|
||||||
|
|
||||||
// utility to turn [red,green,blue] into #rrggbb
|
|
||||||
function rgbToColor(rgb) {
|
|
||||||
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
|
|
||||||
return '#' + (0x1000000 + val).toString(16).slice(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Tinter {
|
|
||||||
constructor() {
|
|
||||||
// The default colour keys to be replaced as referred to in CSS
|
|
||||||
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
|
|
||||||
this.keyRgb = [
|
|
||||||
"rgb(118, 207, 166)", // Vector Green
|
|
||||||
"rgb(234, 245, 240)", // Vector Light Green
|
|
||||||
"rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
|
||||||
];
|
|
||||||
|
|
||||||
// Some algebra workings for calculating the tint % of Vector Green & Light Green
|
|
||||||
// x * 118 + (1 - x) * 255 = 234
|
|
||||||
// x * 118 + 255 - 255 * x = 234
|
|
||||||
// x * 118 - x * 255 = 234 - 255
|
|
||||||
// (255 - 118) x = 255 - 234
|
|
||||||
// x = (255 - 234) / (255 - 118) = 0.16
|
|
||||||
|
|
||||||
// The colour keys to be replaced as referred to in SVGs
|
|
||||||
this.keyHex = [
|
|
||||||
"#76CFA6", // Vector Green
|
|
||||||
"#EAF5F0", // Vector Light Green
|
|
||||||
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
|
|
||||||
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
|
|
||||||
"#000000", // black lowlights of the SVGs (for switching to dark theme)
|
|
||||||
];
|
|
||||||
|
|
||||||
// track the replacement colours actually being used
|
|
||||||
// defaults to our keys.
|
|
||||||
this.colors = [
|
|
||||||
this.keyHex[0],
|
|
||||||
this.keyHex[1],
|
|
||||||
this.keyHex[2],
|
|
||||||
this.keyHex[3],
|
|
||||||
this.keyHex[4],
|
|
||||||
];
|
|
||||||
|
|
||||||
// track the most current tint request inputs (which may differ from the
|
|
||||||
// end result stored in this.colors
|
|
||||||
this.currentTint = [
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
];
|
|
||||||
|
|
||||||
this.cssFixups = [
|
|
||||||
// { theme: {
|
|
||||||
// style: a style object that should be fixed up taken from a stylesheet
|
|
||||||
// attr: name of the attribute to be clobbered, e.g. 'color'
|
|
||||||
// index: ordinal of primary, secondary or tertiary
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
];
|
|
||||||
|
|
||||||
// CSS attributes to be fixed up
|
|
||||||
this.cssAttrs = [
|
|
||||||
"color",
|
|
||||||
"backgroundColor",
|
|
||||||
"borderColor",
|
|
||||||
"borderTopColor",
|
|
||||||
"borderBottomColor",
|
|
||||||
"borderLeftColor",
|
|
||||||
];
|
|
||||||
|
|
||||||
this.svgAttrs = [
|
|
||||||
"fill",
|
|
||||||
"stroke",
|
|
||||||
];
|
|
||||||
|
|
||||||
// List of functions to call when the tint changes.
|
|
||||||
this.tintables = [];
|
|
||||||
|
|
||||||
// the currently loaded theme (if any)
|
|
||||||
this.theme = undefined;
|
|
||||||
|
|
||||||
// whether to force a tint (e.g. after changing theme)
|
|
||||||
this.forceTint = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a callback to fire when the tint changes.
|
|
||||||
* This is used to rewrite the tintable SVGs with the new tint.
|
|
||||||
*
|
|
||||||
* It's not possible to unregister a tintable callback. So this can only be
|
|
||||||
* used to register a static callback. If a set of tintables will change
|
|
||||||
* over time then the best bet is to register a single callback for the
|
|
||||||
* entire set.
|
|
||||||
*
|
|
||||||
* To ensure the tintable work happens at least once, it is also called as
|
|
||||||
* part of registration.
|
|
||||||
*
|
|
||||||
* @param {Function} tintable Function to call when the tint changes.
|
|
||||||
*/
|
|
||||||
registerTintable(tintable) {
|
|
||||||
this.tintables.push(tintable);
|
|
||||||
tintable();
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeyRgb() {
|
|
||||||
return this.keyRgb;
|
|
||||||
}
|
|
||||||
|
|
||||||
tint(primaryColor, secondaryColor, tertiaryColor) {
|
|
||||||
return;
|
|
||||||
// eslint-disable-next-line no-unreachable
|
|
||||||
this.currentTint[0] = primaryColor;
|
|
||||||
this.currentTint[1] = secondaryColor;
|
|
||||||
this.currentTint[2] = tertiaryColor;
|
|
||||||
|
|
||||||
this.calcCssFixups();
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
console.log("Tinter.tint(" + primaryColor + ", " +
|
|
||||||
secondaryColor + ", " +
|
|
||||||
tertiaryColor + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!primaryColor) {
|
|
||||||
primaryColor = this.keyRgb[0];
|
|
||||||
secondaryColor = this.keyRgb[1];
|
|
||||||
tertiaryColor = this.keyRgb[2];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!secondaryColor) {
|
|
||||||
const x = 0.16; // average weighting factor calculated from vector green & light green
|
|
||||||
const rgb = colorToRgb(primaryColor);
|
|
||||||
rgb[0] = x * rgb[0] + (1 - x) * 255;
|
|
||||||
rgb[1] = x * rgb[1] + (1 - x) * 255;
|
|
||||||
rgb[2] = x * rgb[2] + (1 - x) * 255;
|
|
||||||
secondaryColor = rgbToColor(rgb);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!tertiaryColor) {
|
|
||||||
const x = 0.19;
|
|
||||||
const rgb1 = colorToRgb(primaryColor);
|
|
||||||
const rgb2 = colorToRgb(secondaryColor);
|
|
||||||
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
|
|
||||||
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
|
|
||||||
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
|
|
||||||
tertiaryColor = rgbToColor(rgb1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.forceTint == false &&
|
|
||||||
this.colors[0] === primaryColor &&
|
|
||||||
this.colors[1] === secondaryColor &&
|
|
||||||
this.colors[2] === tertiaryColor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.forceTint = false;
|
|
||||||
|
|
||||||
this.colors[0] = primaryColor;
|
|
||||||
this.colors[1] = secondaryColor;
|
|
||||||
this.colors[2] = tertiaryColor;
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
console.log("Tinter.tint final: (" + primaryColor + ", " +
|
|
||||||
secondaryColor + ", " +
|
|
||||||
tertiaryColor + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
// go through manually fixing up the stylesheets.
|
|
||||||
this.applyCssFixups();
|
|
||||||
|
|
||||||
// tell all the SVGs to go fix themselves up
|
|
||||||
// we don't do this as a dispatch otherwise it will visually lag
|
|
||||||
this.tintables.forEach(function(tintable) {
|
|
||||||
tintable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tintSvgWhite(whiteColor) {
|
|
||||||
this.currentTint[3] = whiteColor;
|
|
||||||
|
|
||||||
if (!whiteColor) {
|
|
||||||
whiteColor = this.colors[3];
|
|
||||||
}
|
|
||||||
if (this.colors[3] === whiteColor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.colors[3] = whiteColor;
|
|
||||||
this.tintables.forEach(function(tintable) {
|
|
||||||
tintable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
tintSvgBlack(blackColor) {
|
|
||||||
this.currentTint[4] = blackColor;
|
|
||||||
|
|
||||||
if (!blackColor) {
|
|
||||||
blackColor = this.colors[4];
|
|
||||||
}
|
|
||||||
if (this.colors[4] === blackColor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.colors[4] = blackColor;
|
|
||||||
this.tintables.forEach(function(tintable) {
|
|
||||||
tintable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
setTheme(theme) {
|
|
||||||
this.theme = theme;
|
|
||||||
|
|
||||||
// update keyRgb from the current theme CSS itself, if it defines it
|
|
||||||
if (document.getElementById('mx_theme_accentColor')) {
|
|
||||||
this.keyRgb[0] = window.getComputedStyle(
|
|
||||||
document.getElementById('mx_theme_accentColor')).color;
|
|
||||||
}
|
|
||||||
if (document.getElementById('mx_theme_secondaryAccentColor')) {
|
|
||||||
this.keyRgb[1] = window.getComputedStyle(
|
|
||||||
document.getElementById('mx_theme_secondaryAccentColor')).color;
|
|
||||||
}
|
|
||||||
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
|
|
||||||
this.keyRgb[2] = window.getComputedStyle(
|
|
||||||
document.getElementById('mx_theme_tertiaryAccentColor')).color;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.calcCssFixups();
|
|
||||||
this.forceTint = true;
|
|
||||||
|
|
||||||
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
|
|
||||||
|
|
||||||
if (theme === 'dark') {
|
|
||||||
// abuse the tinter to change all the SVG's #fff to #2d2d2d
|
|
||||||
// XXX: obviously this shouldn't be hardcoded here.
|
|
||||||
this.tintSvgWhite('#2d2d2d');
|
|
||||||
this.tintSvgBlack('#dddddd');
|
|
||||||
} else {
|
|
||||||
this.tintSvgWhite('#ffffff');
|
|
||||||
this.tintSvgBlack('#000000');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
calcCssFixups() {
|
|
||||||
// cache our fixups
|
|
||||||
if (this.cssFixups[this.theme]) return;
|
|
||||||
|
|
||||||
if (DEBUG) {
|
|
||||||
console.debug("calcCssFixups start for " + this.theme + " (checking " +
|
|
||||||
document.styleSheets.length +
|
|
||||||
" stylesheets)");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cssFixups[this.theme] = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
|
||||||
const ss = document.styleSheets[i];
|
|
||||||
try {
|
|
||||||
if (!ss) continue; // well done safari >:(
|
|
||||||
// Chromium apparently sometimes returns null here; unsure why.
|
|
||||||
// see $14534907369972FRXBx:matrix.org in HQ
|
|
||||||
// ...ah, it's because there's a third party extension like
|
|
||||||
// privacybadger inserting its own stylesheet in there with a
|
|
||||||
// resource:// URI or something which results in a XSS error.
|
|
||||||
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
|
|
||||||
// ...except some browsers apparently return stylesheets without
|
|
||||||
// hrefs, which we have no choice but ignore right now
|
|
||||||
|
|
||||||
// XXX seriously? we are hardcoding the name of vector's CSS file in
|
|
||||||
// here?
|
|
||||||
//
|
|
||||||
// Why do we need to limit it to vector's CSS file anyway - if there
|
|
||||||
// are other CSS files affecting the doc don't we want to apply the
|
|
||||||
// same transformations to them?
|
|
||||||
//
|
|
||||||
// Iterating through the CSS looking for matches to hack on feels
|
|
||||||
// pretty horrible anyway. And what if the application skin doesn't use
|
|
||||||
// Vector Green as its primary color?
|
|
||||||
// --richvdh
|
|
||||||
|
|
||||||
// Yes, tinting assumes that you are using the Element skin for now.
|
|
||||||
// The right solution will be to move the CSS over to react-sdk.
|
|
||||||
// And yes, the default assets for the base skin might as well use
|
|
||||||
// Vector Green as any other colour.
|
|
||||||
// --matthew
|
|
||||||
|
|
||||||
// stylesheets we don't have permission to access (eg. ones from extensions) have a null
|
|
||||||
// href and will throw exceptions if we try to access their rules.
|
|
||||||
if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
|
|
||||||
if (ss.disabled) continue;
|
|
||||||
if (!ss.cssRules) continue;
|
|
||||||
|
|
||||||
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
|
|
||||||
|
|
||||||
for (let j = 0; j < ss.cssRules.length; j++) {
|
|
||||||
const rule = ss.cssRules[j];
|
|
||||||
if (!rule.style) continue;
|
|
||||||
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
|
|
||||||
for (let k = 0; k < this.cssAttrs.length; k++) {
|
|
||||||
const attr = this.cssAttrs[k];
|
|
||||||
for (let l = 0; l < this.keyRgb.length; l++) {
|
|
||||||
if (rule.style[attr] === this.keyRgb[l]) {
|
|
||||||
this.cssFixups[this.theme].push({
|
|
||||||
style: rule.style,
|
|
||||||
attr: attr,
|
|
||||||
index: l,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Catch any random exceptions that happen here: all sorts of things can go
|
|
||||||
// wrong with this (nulls, SecurityErrors) and mostly it's for other
|
|
||||||
// stylesheets that we don't want to proces anyway. We should not propagate an
|
|
||||||
// exception out since this will cause the app to fail to start.
|
|
||||||
console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (DEBUG) {
|
|
||||||
console.log("calcCssFixups end (" +
|
|
||||||
this.cssFixups[this.theme].length +
|
|
||||||
" fixups)");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
applyCssFixups() {
|
|
||||||
if (DEBUG) {
|
|
||||||
console.log("applyCssFixups start (" +
|
|
||||||
this.cssFixups[this.theme].length +
|
|
||||||
" fixups)");
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
|
|
||||||
const cssFixup = this.cssFixups[this.theme][i];
|
|
||||||
try {
|
|
||||||
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
|
|
||||||
} catch (e) {
|
|
||||||
// Firefox Quantum explodes if you manually edit the CSS in the
|
|
||||||
// inspector and then try to do a tint, as apparently all the
|
|
||||||
// fixups are then stale.
|
|
||||||
console.error("Failed to apply cssFixup in Tinter! ", e.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (DEBUG) console.log("applyCssFixups end");
|
|
||||||
}
|
|
||||||
|
|
||||||
// XXX: we could just move this all into TintableSvg, but as it's so similar
|
|
||||||
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
|
|
||||||
// keeping it here for now.
|
|
||||||
calcSvgFixups(svgs) {
|
|
||||||
// go through manually fixing up SVG colours.
|
|
||||||
// we could do this by stylesheets, but keeping the stylesheets
|
|
||||||
// updated would be a PITA, so just brute-force search for the
|
|
||||||
// key colour; cache the element and apply.
|
|
||||||
|
|
||||||
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
|
|
||||||
const fixups = [];
|
|
||||||
for (let i = 0; i < svgs.length; i++) {
|
|
||||||
let svgDoc;
|
|
||||||
try {
|
|
||||||
svgDoc = svgs[i].contentDocument;
|
|
||||||
} catch (e) {
|
|
||||||
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
|
|
||||||
if (e.message) {
|
|
||||||
msg += e.message;
|
|
||||||
}
|
|
||||||
if (e.stack) {
|
|
||||||
msg += ' | stack: ' + e.stack;
|
|
||||||
}
|
|
||||||
console.error(msg);
|
|
||||||
}
|
|
||||||
if (!svgDoc) continue;
|
|
||||||
const tags = svgDoc.getElementsByTagName("*");
|
|
||||||
for (let j = 0; j < tags.length; j++) {
|
|
||||||
const tag = tags[j];
|
|
||||||
for (let k = 0; k < this.svgAttrs.length; k++) {
|
|
||||||
const attr = this.svgAttrs[k];
|
|
||||||
for (let l = 0; l < this.keyHex.length; l++) {
|
|
||||||
if (tag.getAttribute(attr) &&
|
|
||||||
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
|
|
||||||
fixups.push({
|
|
||||||
node: tag,
|
|
||||||
attr: attr,
|
|
||||||
index: l,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (DEBUG) console.log("calcSvgFixups end");
|
|
||||||
|
|
||||||
return fixups;
|
|
||||||
}
|
|
||||||
|
|
||||||
applySvgFixups(fixups) {
|
|
||||||
if (DEBUG) console.log("applySvgFixups start for " + fixups);
|
|
||||||
for (let i = 0; i < fixups.length; i++) {
|
|
||||||
const svgFixup = fixups[i];
|
|
||||||
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
|
|
||||||
}
|
|
||||||
if (DEBUG) console.log("applySvgFixups end");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.singletonTinter === undefined) {
|
|
||||||
global.singletonTinter = new Tinter();
|
|
||||||
}
|
|
||||||
export default global.singletonTinter;
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015 - 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
|
|
||||||
|
import { MatrixClientPeg } from "./MatrixClientPeg";
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
import { haveTileForEvent } from "./components/views/rooms/EventTile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true iff this event arriving in a room should affect the room's
|
* Returns true iff this event arriving in a room should affect the room's
|
||||||
|
@ -25,28 +29,27 @@ import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
||||||
* @param {Object} ev The event
|
* @param {Object} ev The event
|
||||||
* @returns {boolean} True if the given event should affect the unread message count
|
* @returns {boolean} True if the given event should affect the unread message count
|
||||||
*/
|
*/
|
||||||
export function eventTriggersUnreadCount(ev) {
|
export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
return false;
|
return false;
|
||||||
} else if (ev.getType() == 'm.room.member') {
|
|
||||||
return false;
|
|
||||||
} else if (ev.getType() == 'm.room.third_party_invite') {
|
|
||||||
return false;
|
|
||||||
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
|
|
||||||
return false;
|
|
||||||
} else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
|
|
||||||
return false;
|
|
||||||
} else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') {
|
|
||||||
return false;
|
|
||||||
} else if (ev.getType() == 'm.room.server_acl') {
|
|
||||||
return false;
|
|
||||||
} else if (ev.isRedacted()) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch (ev.getType()) {
|
||||||
|
case EventType.RoomMember:
|
||||||
|
case EventType.RoomThirdPartyInvite:
|
||||||
|
case EventType.CallAnswer:
|
||||||
|
case EventType.CallHangup:
|
||||||
|
case EventType.RoomAliases:
|
||||||
|
case EventType.RoomCanonicalAlias:
|
||||||
|
case EventType.RoomServerAcl:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ev.isRedacted()) return false;
|
||||||
return haveTileForEvent(ev);
|
return haveTileForEvent(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function doesRoomHaveUnreadMessages(room) {
|
export function doesRoomHaveUnreadMessages(room: Room): boolean {
|
||||||
const myUserId = MatrixClientPeg.get().getUserId();
|
const myUserId = MatrixClientPeg.get().getUserId();
|
||||||
|
|
||||||
// get the most recent read receipt sent by our account.
|
// get the most recent read receipt sent by our account.
|