Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/17686
Conflicts: src/components/views/elements/StyledRadioGroup.tsx src/components/views/settings/tabs/room/SecurityRoomSettingsTab.tsx
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)
|
||||||
|
|
21
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": {
|
||||||
|
@ -78,8 +78,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 +89,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",
|
||||||
|
@ -123,6 +123,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,19 +133,20 @@
|
||||||
"@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.14.0",
|
||||||
"@typescript-eslint/parser": "^4.14.0",
|
"@typescript-eslint/parser": "^4.14.0",
|
||||||
|
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
|
||||||
"babel-eslint": "^10.1.0",
|
"babel-eslint": "^10.1.0",
|
||||||
"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-matrix-org": "^0.2.0",
|
||||||
"eslint-plugin-babel": "^5.3.1",
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
|
@ -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"
|
||||||
|
|
|
@ -123,7 +123,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";
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -134,8 +134,9 @@ limitations under the License.
|
||||||
.mx_Toast_buttons {
|
.mx_Toast_buttons {
|
||||||
float: right;
|
float: right;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
.mx_FormButton {
|
.mx_AccessibleButton {
|
||||||
min-width: 96px;
|
min-width: 96px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -33,9 +33,14 @@ limitations under the License.
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
contain: content;
|
||||||
|
|
||||||
.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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -114,6 +114,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;
|
||||||
|
|
|
@ -181,6 +181,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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -3,6 +3,6 @@
|
||||||
# docker push vectorim/element-web-ci-e2etests-env:latest
|
# docker push vectorim/element-web-ci-e2etests-env:latest
|
||||||
FROM node:14-buster
|
FROM node:14-buster
|
||||||
RUN apt-get update
|
RUN apt-get update
|
||||||
RUN apt-get -y install 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.
|
||||||
|
|
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[];
|
||||||
|
}
|
||||||
|
}
|
15
src/@types/global.d.ts
vendored
|
@ -44,6 +44,7 @@ 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";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -84,6 +85,7 @@ declare global {
|
||||||
mxPerformanceMonitor: PerformanceMonitor;
|
mxPerformanceMonitor: PerformanceMonitor;
|
||||||
mxPerformanceEntryNames: any;
|
mxPerformanceEntryNames: any;
|
||||||
mxUIStore: UIStore;
|
mxUIStore: UIStore;
|
||||||
|
mxSetupEncryptionStore?: SetupEncryptionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
|
@ -111,19 +113,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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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");
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -307,7 +307,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,
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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,13 +125,13 @@ 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: {},
|
||||||
|
@ -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;
|
||||||
|
@ -351,13 +352,6 @@ 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;
|
||||||
|
@ -367,6 +361,14 @@ interface IOpts {
|
||||||
ref?: React.Ref<any>;
|
ref?: React.Ref<any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IOptsReturnNode extends IOpts {
|
||||||
|
returnString: false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IOptsReturnString extends IOpts {
|
||||||
|
returnString: true;
|
||||||
|
}
|
||||||
|
|
||||||
/* turn a matrix event body into html
|
/* turn a matrix event body into html
|
||||||
*
|
*
|
||||||
* content: 'content' of the MatrixEvent
|
* content: 'content' of the MatrixEvent
|
||||||
|
@ -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;
|
||||||
|
@ -501,7 +505,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 +516,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 +527,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 +538,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":
|
||||||
|
|
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");
|
||||||
|
}
|
||||||
|
}
|
|
@ -385,7 +385,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());
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,15 +41,15 @@ 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(
|
||||||
|
@ -49,7 +58,7 @@ export function showStartChatInviteDialog(initialText) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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,9 +119,14 @@ 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
|
||||||
|
@ -126,19 +140,47 @@ export function showAnyInviteErrors(addrs, room, inviter) {
|
||||||
} 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;
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -114,7 +116,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;
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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';
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
|
|
|
@ -17,8 +17,8 @@ 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';
|
||||||
|
@ -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();
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,8 @@ 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 React from 'react';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
import * as Roles from './Roles';
|
import * as Roles from './Roles';
|
||||||
|
@ -20,6 +22,11 @@ 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;
|
||||||
}
|
}
|
||||||
|
@ -466,9 +486,33 @@ 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 {
|
||||||
|
@ -594,7 +638,7 @@ function textForMjolnirEvent(event): () => string | null {
|
||||||
}
|
}
|
||||||
|
|
||||||
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)?.() || '';
|
||||||
}
|
}
|
|
@ -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.
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 New Vector Ltd
|
Copyright 2017 - 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,15 +14,19 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const emailRegex = /^\S+@\S+\.\S+$/;
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
|
const emailRegex = /^\S+@\S+\.\S+$/;
|
||||||
const mxUserIdRegex = /^@\S+:\S+$/;
|
const mxUserIdRegex = /^@\S+:\S+$/;
|
||||||
const mxRoomIdRegex = /^!\S+:\S+$/;
|
const mxRoomIdRegex = /^!\S+:\S+$/;
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
|
||||||
export const addressTypes = [
|
|
||||||
'mx-user-id', 'mx-room-id', 'email',
|
export enum AddressType {
|
||||||
];
|
Email = "email",
|
||||||
|
MatrixUserId = "mx-user-id",
|
||||||
|
MatrixRoomId = "mx-room-id",
|
||||||
|
}
|
||||||
|
|
||||||
// PropType definition for an object describing
|
// PropType definition for an object describing
|
||||||
// an address that can be invited to a room (which
|
// an address that can be invited to a room (which
|
||||||
|
@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({
|
||||||
isKnown: PropTypes.bool,
|
isKnown: PropTypes.bool,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getAddressType(inputText) {
|
export function getAddressType(inputText: string): AddressType | null {
|
||||||
const isEmailAddress = emailRegex.test(inputText);
|
if (emailRegex.test(inputText)) {
|
||||||
const isUserId = mxUserIdRegex.test(inputText);
|
return AddressType.Email;
|
||||||
const isRoomId = mxRoomIdRegex.test(inputText);
|
} else if (mxUserIdRegex.test(inputText)) {
|
||||||
|
return AddressType.MatrixUserId;
|
||||||
// sanity check the input for user IDs
|
} else if (mxRoomIdRegex.test(inputText)) {
|
||||||
if (isEmailAddress) {
|
return AddressType.MatrixRoomId;
|
||||||
return 'email';
|
|
||||||
} else if (isUserId) {
|
|
||||||
return 'mx-user-id';
|
|
||||||
} else if (isRoomId) {
|
|
||||||
return 'mx-room-id';
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
|
@ -57,6 +57,8 @@ export enum Modifiers {
|
||||||
|
|
||||||
// Meta-modifier: isMac ? CMD : CONTROL
|
// Meta-modifier: isMac ? CMD : CONTROL
|
||||||
export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL;
|
export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL;
|
||||||
|
// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts
|
||||||
|
export const DIGITS = "digits";
|
||||||
|
|
||||||
interface IKeybind {
|
interface IKeybind {
|
||||||
modifiers?: Modifiers[];
|
modifiers?: Modifiers[];
|
||||||
|
@ -319,6 +321,7 @@ const alternateKeyName: Record<string, string> = {
|
||||||
[Key.SPACE]: _td("Space"),
|
[Key.SPACE]: _td("Space"),
|
||||||
[Key.HOME]: _td("Home"),
|
[Key.HOME]: _td("Home"),
|
||||||
[Key.END]: _td("End"),
|
[Key.END]: _td("End"),
|
||||||
|
[DIGITS]: _td("[number]"),
|
||||||
};
|
};
|
||||||
const keyIcon: Record<string, string> = {
|
const keyIcon: Record<string, string> = {
|
||||||
[Key.ARROW_UP]: "↑",
|
[Key.ARROW_UP]: "↑",
|
||||||
|
|
|
@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {ReactElement} from 'react';
|
import { ReactElement } from 'react';
|
||||||
import Room from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
|
||||||
import CommandProvider from './CommandProvider';
|
import CommandProvider from './CommandProvider';
|
||||||
import CommunityProvider from './CommunityProvider';
|
import CommunityProvider from './CommunityProvider';
|
||||||
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
import DuckDuckGoProvider from './DuckDuckGoProvider';
|
||||||
|
@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider';
|
||||||
import UserProvider from './UserProvider';
|
import UserProvider from './UserProvider';
|
||||||
import EmojiProvider from './EmojiProvider';
|
import EmojiProvider from './EmojiProvider';
|
||||||
import NotifProvider from './NotifProvider';
|
import NotifProvider from './NotifProvider';
|
||||||
import {timeout} from "../utils/promise";
|
import { timeout } from "../utils/promise";
|
||||||
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
import SpaceProvider from "./SpaceProvider";
|
import SpaceProvider from "./SpaceProvider";
|
||||||
|
@ -54,13 +55,14 @@ const PROVIDERS = [
|
||||||
EmojiProvider,
|
EmojiProvider,
|
||||||
NotifProvider,
|
NotifProvider,
|
||||||
CommandProvider,
|
CommandProvider,
|
||||||
CommunityProvider,
|
|
||||||
DuckDuckGoProvider,
|
DuckDuckGoProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
|
||||||
if (SettingsStore.getValue("feature_spaces")) {
|
if (SettingsStore.getValue("feature_spaces")) {
|
||||||
PROVIDERS.push(SpaceProvider);
|
PROVIDERS.push(SpaceProvider);
|
||||||
|
} else {
|
||||||
|
PROVIDERS.push(CommunityProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Providers will get rejected if they take longer than this.
|
// Providers will get rejected if they take longer than this.
|
||||||
|
|
|
@ -15,7 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Room from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
|
|
|
@ -17,28 +17,24 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import {uniqBy, sortBy} from "lodash";
|
import { uniqBy, sortBy } from "lodash";
|
||||||
import Room from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import { PillCompletion } from './Components';
|
||||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
|
||||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
import { ICompletion, ISelectionRange } from "./Autocompleter";
|
||||||
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
import RoomAvatar from '../components/views/avatars/RoomAvatar';
|
||||||
import SettingsStore from "../settings/SettingsStore";
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
const ROOM_REGEX = /\B#\S*/g;
|
const ROOM_REGEX = /\B#\S*/g;
|
||||||
|
|
||||||
function score(query: string, space: string) {
|
// Prefer canonical aliases over non-canonical ones
|
||||||
const index = space.indexOf(query);
|
function canonicalScore(displayedAlias: string, room: Room): number {
|
||||||
if (index === -1) {
|
return displayedAlias === room.getCanonicalAlias() ? 0 : 1;
|
||||||
return Infinity;
|
|
||||||
} else {
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function matcherObject(room: Room, displayedAlias: string, matchName = "") {
|
function matcherObject(room: Room, displayedAlias: string, matchName = "") {
|
||||||
|
@ -106,7 +102,7 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
const matchedString = command[0];
|
const matchedString = command[0];
|
||||||
completions = this.matcher.match(matchedString, limit);
|
completions = this.matcher.match(matchedString, limit);
|
||||||
completions = sortBy(completions, [
|
completions = sortBy(completions, [
|
||||||
(c) => score(matchedString, c.displayedAlias),
|
(c) => canonicalScore(c.displayedAlias, c.room),
|
||||||
(c) => c.displayedAlias.length,
|
(c) => c.displayedAlias.length,
|
||||||
]);
|
]);
|
||||||
completions = uniqBy(completions, (match) => match.room);
|
completions = uniqBy(completions, (match) => match.room);
|
||||||
|
|
|
@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React, { HTMLAttributes } from "react";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps extends HTMLAttributes<HTMLDivElement> {
|
||||||
className?: string;
|
className?: string;
|
||||||
onScroll?: () => void;
|
onScroll?: () => void;
|
||||||
onWheel?: () => void;
|
onWheel?: () => void;
|
||||||
|
@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
|
||||||
|
|
||||||
return (<div
|
return (<div
|
||||||
|
{...otherProps}
|
||||||
ref={this.containerRef}
|
ref={this.containerRef}
|
||||||
style={this.props.style}
|
style={style}
|
||||||
className={["mx_AutoHideScrollbar", this.props.className].join(" ")}
|
className={["mx_AutoHideScrollbar", className].join(" ")}
|
||||||
onWheel={this.props.onWheel}
|
onWheel={onWheel}
|
||||||
tabIndex={this.props.tabIndex}
|
tabIndex={tabIndex}
|
||||||
>
|
>
|
||||||
{ this.props.children }
|
{ children }
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,6 @@ import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
|
|
||||||
import { Droppable } from 'react-beautiful-dnd';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
|
@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMouseDown = e => {
|
onClick = e => {
|
||||||
// only dispatch if its not a no-op
|
// only dispatch if its not a no-op
|
||||||
if (this.state.selectedTags.length > 0) {
|
if (this.state.selectedTags.length > 0) {
|
||||||
dis.dispatch({action: 'deselect_tags'});
|
dis.dispatch({action: 'deselect_tags'});
|
||||||
|
@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
|
||||||
return <div className={classes} onClick={this.onClearFilterClick}>
|
return <div className={classes} onClick={this.onClearFilterClick}>
|
||||||
<AutoHideScrollbar
|
<AutoHideScrollbar
|
||||||
className="mx_GroupFilterPanel_scroller"
|
className="mx_GroupFilterPanel_scroller"
|
||||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
onClick={this.onClick}
|
||||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
|
|
||||||
onMouseDown={this.onMouseDown}
|
|
||||||
>
|
>
|
||||||
<Droppable
|
<div className="mx_GroupFilterPanel_tagTileContainer">
|
||||||
droppableId="tag-panel-droppable"
|
{ this.renderGlobalIcon() }
|
||||||
type="draggable-TagTile"
|
{ tags }
|
||||||
>
|
<div>
|
||||||
{ (provided, snapshot) => (
|
{ createButton }
|
||||||
<div
|
</div>
|
||||||
className="mx_GroupFilterPanel_tagTileContainer"
|
</div>
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
|
||||||
{ this.renderGlobalIcon() }
|
|
||||||
{ tags }
|
|
||||||
<div>
|
|
||||||
{createButton}
|
|
||||||
</div>
|
|
||||||
{ provided.placeholder }
|
|
||||||
</div>
|
|
||||||
) }
|
|
||||||
</Droppable>
|
|
||||||
</AutoHideScrollbar>
|
</AutoHideScrollbar>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
|
||||||
|
|
||||||
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
|
||||||
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
|
||||||
const leftOverflowIndicator = this.props.trackHorizontalOverflow
|
const leftOverflowIndicator = trackHorizontalOverflow
|
||||||
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
|
||||||
const rightOverflowIndicator = this.props.trackHorizontalOverflow
|
const rightOverflowIndicator = trackHorizontalOverflow
|
||||||
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
|
||||||
|
|
||||||
return (<AutoHideScrollbar
|
return (<AutoHideScrollbar
|
||||||
ref={this._collectScrollerComponent}
|
ref={this._collectScrollerComponent}
|
||||||
wrappedRef={this._collectScroller}
|
wrappedRef={this._collectScroller}
|
||||||
onWheel={this.onMouseWheel}
|
onWheel={this.onMouseWheel}
|
||||||
{...this.props}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
{ leftOverflowIndicator }
|
{ leftOverflowIndicator }
|
||||||
{ this.props.children }
|
{ children }
|
||||||
{ rightOverflowIndicator }
|
{ rightOverflowIndicator }
|
||||||
</AutoHideScrollbar>);
|
</AutoHideScrollbar>);
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import CustomRoomTagPanel from "./CustomRoomTagPanel";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import RoomList from "../views/rooms/RoomList";
|
import RoomList from "../views/rooms/RoomList";
|
||||||
|
import CallHandler from "../../CallHandler";
|
||||||
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
import { HEADER_HEIGHT } from "../views/rooms/RoomSublist";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import UserMenu from "./UserMenu";
|
import UserMenu from "./UserMenu";
|
||||||
|
@ -124,6 +125,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
this.setState({ activeSpace });
|
this.setState({ activeSpace });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onDialPad = () => {
|
||||||
|
dis.fire(Action.OpenDialPad);
|
||||||
|
}
|
||||||
|
|
||||||
private onExplore = () => {
|
private onExplore = () => {
|
||||||
dis.fire(Action.ViewRoomDirectory);
|
dis.fire(Action.ViewRoomDirectory);
|
||||||
};
|
};
|
||||||
|
@ -397,7 +402,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderSearchExplore(): React.ReactNode {
|
private renderSearchDialExplore(): React.ReactNode {
|
||||||
|
let dialPadButton = null;
|
||||||
|
|
||||||
|
// If we have dialer support, show a button to bring up the dial pad
|
||||||
|
// to start a new call
|
||||||
|
if (CallHandler.sharedInstance().getSupportsPstnProtocol()) {
|
||||||
|
dialPadButton =
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={classNames("mx_LeftPanel_dialPadButton", {})}
|
||||||
|
onClick={this.onDialPad}
|
||||||
|
title={_t("Open dial pad")}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="mx_LeftPanel_filterContainer"
|
className="mx_LeftPanel_filterContainer"
|
||||||
|
@ -410,6 +428,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
onKeyDown={this.onKeyDown}
|
onKeyDown={this.onKeyDown}
|
||||||
onSelectRoom={this.selectRoom}
|
onSelectRoom={this.selectRoom}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{dialPadButton}
|
||||||
|
|
||||||
<AccessibleTooltipButton
|
<AccessibleTooltipButton
|
||||||
className={classNames("mx_LeftPanel_exploreButton", {
|
className={classNames("mx_LeftPanel_exploreButton", {
|
||||||
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
mx_LeftPanel_exploreButton_space: !!this.state.activeSpace,
|
||||||
|
@ -458,7 +479,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
{leftLeftPanel}
|
{leftLeftPanel}
|
||||||
<aside className="mx_LeftPanel_roomListContainer">
|
<aside className="mx_LeftPanel_roomListContainer">
|
||||||
{this.renderHeader()}
|
{this.renderHeader()}
|
||||||
{this.renderSearchExplore()}
|
{this.renderSearchDialExplore()}
|
||||||
{this.renderBreadcrumbs()}
|
{this.renderBreadcrumbs()}
|
||||||
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
|
||||||
<div className="mx_LeftPanel_roomListWrapper">
|
<div className="mx_LeftPanel_roomListWrapper">
|
||||||
|
|
|
@ -19,19 +19,16 @@ limitations under the License.
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as PropTypes from 'prop-types';
|
import * as PropTypes from 'prop-types';
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
import { DragDropContext } from 'react-beautiful-dnd';
|
|
||||||
|
|
||||||
import {Key} from '../../Keyboard';
|
import {Key} from '../../Keyboard';
|
||||||
import PageTypes from '../../PageTypes';
|
import PageTypes from '../../PageTypes';
|
||||||
import CallMediaHandler from '../../CallMediaHandler';
|
import MediaDeviceHandler from '../../MediaDeviceHandler';
|
||||||
import { fixupColorFonts } from '../../utils/FontManager';
|
import { fixupColorFonts } from '../../utils/FontManager';
|
||||||
import * as sdk from '../../index';
|
import * as sdk from '../../index';
|
||||||
import dis from '../../dispatcher/dispatcher';
|
import dis from '../../dispatcher/dispatcher';
|
||||||
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
import { IMatrixClientCreds } from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
|
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
|
||||||
import RoomListActions from '../../actions/RoomListActions';
|
|
||||||
import ResizeHandle from '../views/elements/ResizeHandle';
|
import ResizeHandle from '../views/elements/ResizeHandle';
|
||||||
import {Resizer, CollapseDistributor} from '../../resizer';
|
import {Resizer, CollapseDistributor} from '../../resizer';
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
|
@ -170,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
// stash the MatrixClient in case we log out before we are unmounted
|
// stash the MatrixClient in case we log out before we are unmounted
|
||||||
this._matrixClient = this.props.matrixClient;
|
this._matrixClient = this.props.matrixClient;
|
||||||
|
|
||||||
CallMediaHandler.loadDevices();
|
MediaDeviceHandler.loadDevices();
|
||||||
|
|
||||||
fixupColorFonts();
|
fixupColorFonts();
|
||||||
|
|
||||||
|
@ -569,50 +566,6 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDragEnd = (result) => {
|
|
||||||
// Dragged to an invalid destination, not onto a droppable
|
|
||||||
if (!result.destination) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dest = result.destination.droppableId;
|
|
||||||
|
|
||||||
if (dest === 'tag-panel-droppable') {
|
|
||||||
// Could be "GroupTile +groupId:domain"
|
|
||||||
const draggableId = result.draggableId.split(' ').pop();
|
|
||||||
|
|
||||||
// Dispatch synchronously so that the GroupFilterPanel receives an
|
|
||||||
// optimistic update from GroupFilterOrderStore before the previous
|
|
||||||
// state is shown.
|
|
||||||
dis.dispatch(TagOrderActions.moveTag(
|
|
||||||
this._matrixClient,
|
|
||||||
draggableId,
|
|
||||||
result.destination.index,
|
|
||||||
), true);
|
|
||||||
} else if (dest.startsWith('room-sub-list-droppable_')) {
|
|
||||||
this._onRoomTileEndDrag(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
_onRoomTileEndDrag = (result) => {
|
|
||||||
let newTag = result.destination.droppableId.split('_')[1];
|
|
||||||
let prevTag = result.source.droppableId.split('_')[1];
|
|
||||||
if (newTag === 'undefined') newTag = undefined;
|
|
||||||
if (prevTag === 'undefined') prevTag = undefined;
|
|
||||||
|
|
||||||
const roomId = result.draggableId.split('_')[1];
|
|
||||||
|
|
||||||
const oldIndex = result.source.index;
|
|
||||||
const newIndex = result.destination.index;
|
|
||||||
|
|
||||||
dis.dispatch(RoomListActions.tagRoom(
|
|
||||||
this._matrixClient,
|
|
||||||
this._matrixClient.getRoom(roomId),
|
|
||||||
prevTag, newTag,
|
|
||||||
oldIndex, newIndex,
|
|
||||||
), true);
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const RoomView = sdk.getComponent('structures.RoomView');
|
const RoomView = sdk.getComponent('structures.RoomView');
|
||||||
const UserView = sdk.getComponent('structures.UserView');
|
const UserView = sdk.getComponent('structures.UserView');
|
||||||
|
@ -679,17 +632,15 @@ class LoggedInView extends React.Component<IProps, IState> {
|
||||||
aria-hidden={this.props.hideToSRUsers}
|
aria-hidden={this.props.hideToSRUsers}
|
||||||
>
|
>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<DragDropContext onDragEnd={this._onDragEnd}>
|
<div ref={this._resizeContainer} className={bodyClasses}>
|
||||||
<div ref={this._resizeContainer} className={bodyClasses}>
|
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
||||||
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
|
<LeftPanel
|
||||||
<LeftPanel
|
isMinimized={this.props.collapseLhs || false}
|
||||||
isMinimized={this.props.collapseLhs || false}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
/>
|
||||||
/>
|
<ResizeHandle />
|
||||||
<ResizeHandle />
|
{ pageElement }
|
||||||
{ pageElement }
|
</div>
|
||||||
</div>
|
|
||||||
</DragDropContext>
|
|
||||||
</div>
|
</div>
|
||||||
<CallContainer />
|
<CallContainer />
|
||||||
<NonUrgentToastContainer />
|
<NonUrgentToastContainer />
|
||||||
|
|
|
@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom";
|
||||||
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
import {_t, _td, getCurrentLanguage} from '../../languageHandler';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import ThemeController from "../../settings/controllers/ThemeController";
|
import ThemeController from "../../settings/controllers/ThemeController";
|
||||||
import { startAnyRegistrationFlow } from "../../Registration.js";
|
import { startAnyRegistrationFlow } from "../../Registration";
|
||||||
import { messageForSyncError } from '../../utils/ErrorUtils';
|
import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||||
|
@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
});
|
});
|
||||||
|
|
||||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
|
||||||
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
|
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
|
||||||
}, (errorCode) => {
|
}, (errorCode) => {
|
||||||
// Map JS-SDK error codes to tracker codes for aggregation
|
// Map JS-SDK error codes to tracker codes for aggregation
|
||||||
|
|
|
@ -38,6 +38,7 @@ import defaultDispatcher from '../../dispatcher/dispatcher';
|
||||||
|
|
||||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||||
|
const membershipTypes = ['m.room.member', 'm.room.third_party_invite', 'm.room.server_acl'];
|
||||||
|
|
||||||
// check if there is a previous event and it has the same sender as this event
|
// check if there is a previous event and it has the same sender as this event
|
||||||
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
// and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL
|
||||||
|
@ -66,8 +67,6 @@ function shouldFormContinuation(prevEvent, mxEvent) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType() === 'm.room.third_party_invite';
|
|
||||||
|
|
||||||
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
/* (almost) stateless UI component which builds the event tiles in the room timeline.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("structures.MessagePanel")
|
@replaceableComponent("structures.MessagePanel")
|
||||||
|
@ -1144,7 +1143,7 @@ class RedactionGrouper {
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
class MemberGrouper {
|
class MemberGrouper {
|
||||||
static canStartGroup = function(panel, ev) {
|
static canStartGroup = function(panel, ev) {
|
||||||
return panel._shouldShowEvent(ev) && isMembershipChange(ev);
|
return panel._shouldShowEvent(ev) && membershipTypes.includes(ev.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(panel, ev, prevEvent, lastShownEvent) {
|
constructor(panel, ev, prevEvent, lastShownEvent) {
|
||||||
|
@ -1162,7 +1161,7 @@ class MemberGrouper {
|
||||||
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
|
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return isMembershipChange(ev);
|
return membershipTypes.includes(ev.getType());
|
||||||
}
|
}
|
||||||
|
|
||||||
add(ev) {
|
add(ev) {
|
||||||
|
|
|
@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{ _t(
|
{ _t(
|
||||||
"To set up a filter, drag a community avatar over to the filter panel on " +
|
"You can click on an avatar in the " +
|
||||||
"the far left hand side of the screen. You can click on an avatar in the " +
|
|
||||||
"filter panel at any time to see only the rooms and people associated " +
|
"filter panel at any time to see only the rooms and people associated " +
|
||||||
"with that community.",
|
"with that community.",
|
||||||
) }
|
) }
|
||||||
|
|
|
@ -337,11 +337,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
|
||||||
|
// If room was shift-clicked, remove it from the room directory
|
||||||
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
if (ev.shiftKey && !this.state.selectedCommunityId) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.removeFromDirectory(room);
|
this.removeFromDirectory(room);
|
||||||
} else {
|
|
||||||
this.showRoom(room);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -568,11 +567,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
let avatarUrl = null;
|
let avatarUrl = null;
|
||||||
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
|
||||||
|
|
||||||
|
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
|
||||||
return [
|
return [
|
||||||
<div key={ `${room.room_id}_avatar` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_avatar` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_roomAvatar"
|
className="mx_RoomDirectory_roomAvatar"
|
||||||
>
|
>
|
||||||
<BaseAvatar
|
<BaseAvatar
|
||||||
|
@ -584,42 +583,50 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
|
||||||
url={avatarUrl}
|
url={avatarUrl}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_description` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_description` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_roomDescription"
|
className="mx_RoomDirectory_roomDescription"
|
||||||
>
|
>
|
||||||
<div className="mx_RoomDirectory_name">{ name }</div>
|
<div
|
||||||
<div className="mx_RoomDirectory_topic"
|
className="mx_RoomDirectory_name"
|
||||||
onClick={ (ev) => { ev.stopPropagation(); } }
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
>
|
||||||
|
{ name }
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="mx_RoomDirectory_topic"
|
||||||
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
dangerouslySetInnerHTML={{ __html: topic }}
|
dangerouslySetInnerHTML={{ __html: topic }}
|
||||||
/>
|
/>
|
||||||
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div>
|
<div
|
||||||
|
className="mx_RoomDirectory_alias"
|
||||||
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
|
>
|
||||||
|
{ getDisplayAliasForRoom(room) }
|
||||||
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_memberCount` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_memberCount` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_roomMemberCount"
|
className="mx_RoomDirectory_roomMemberCount"
|
||||||
>
|
>
|
||||||
{ room.num_joined_members }
|
{ room.num_joined_members }
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_preview` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_preview` }
|
||||||
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
// cancel onMouseDown otherwise shift-clicking highlights text
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_preview"
|
className="mx_RoomDirectory_preview"
|
||||||
>
|
>
|
||||||
{previewButton}
|
{ previewButton }
|
||||||
</div>,
|
</div>,
|
||||||
<div key={ `${room.room_id}_join` }
|
<div
|
||||||
onClick={(ev) => this.onRoomClicked(room, ev)}
|
key={ `${room.room_id}_join` }
|
||||||
// cancel onMouseDown otherwise shift-clicking highlights text
|
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
|
||||||
onMouseDown={(ev) => {ev.preventDefault();}}
|
|
||||||
className="mx_RoomDirectory_join"
|
className="mx_RoomDirectory_join"
|
||||||
>
|
>
|
||||||
{joinOrViewButton}
|
{ joinOrViewButton }
|
||||||
</div>,
|
</div>,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
|
||||||
import { EventSubscription } from "fbemitter";
|
import { EventSubscription } from "fbemitter";
|
||||||
|
@ -60,7 +60,7 @@ import ScrollPanel from "./ScrollPanel";
|
||||||
import TimelinePanel from "./TimelinePanel";
|
import TimelinePanel from "./TimelinePanel";
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
|
||||||
import SearchBar from "../views/rooms/SearchBar";
|
import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
|
||||||
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
||||||
import AuxPanel from "../views/rooms/AuxPanel";
|
import AuxPanel from "../views/rooms/AuxPanel";
|
||||||
import RoomHeader from "../views/rooms/RoomHeader";
|
import RoomHeader from "../views/rooms/RoomHeader";
|
||||||
|
@ -82,6 +82,7 @@ import SpaceRoomView from "./SpaceRoomView";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
|
import EditorStateTransfer from "../../utils/EditorStateTransfer";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
let debuglog = function(msg: string) {};
|
let debuglog = function(msg: string) {};
|
||||||
|
@ -139,7 +140,7 @@ export interface IState {
|
||||||
draggingFile: boolean;
|
draggingFile: boolean;
|
||||||
searching: boolean;
|
searching: boolean;
|
||||||
searchTerm?: string;
|
searchTerm?: string;
|
||||||
searchScope?: "All" | "Room";
|
searchScope?: SearchScope;
|
||||||
searchResults?: XOR<{}, {
|
searchResults?: XOR<{}, {
|
||||||
count: number;
|
count: number;
|
||||||
highlights: string[];
|
highlights: string[];
|
||||||
|
@ -172,11 +173,7 @@ export interface IState {
|
||||||
// We load this later by asking the js-sdk to suggest a version for us.
|
// We load this later by asking the js-sdk to suggest a version for us.
|
||||||
// This object is the result of Room#getRecommendedVersion()
|
// This object is the result of Room#getRecommendedVersion()
|
||||||
|
|
||||||
upgradeRecommendation?: {
|
upgradeRecommendation?: IRecommendedVersion;
|
||||||
version: string;
|
|
||||||
needsUpgrade: boolean;
|
|
||||||
urgent: boolean;
|
|
||||||
};
|
|
||||||
canReact: boolean;
|
canReact: boolean;
|
||||||
canReply: boolean;
|
canReply: boolean;
|
||||||
layout: Layout;
|
layout: Layout;
|
||||||
|
@ -196,6 +193,7 @@ export interface IState {
|
||||||
// whether or not a spaces context switch brought us here,
|
// whether or not a spaces context switch brought us here,
|
||||||
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
// if it did we don't want the room to be marked as read as soon as it is loaded.
|
||||||
wasContextSwitch?: boolean;
|
wasContextSwitch?: boolean;
|
||||||
|
editState?: EditorStateTransfer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomView")
|
@replaceableComponent("structures.RoomView")
|
||||||
|
@ -819,6 +817,36 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
case 'focus_search':
|
case 'focus_search':
|
||||||
this.onSearchClick();
|
this.onSearchClick();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "edit_event": {
|
||||||
|
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||||
|
this.setState({ editState }, () => {
|
||||||
|
if (payload.event) {
|
||||||
|
this.messagePanel?.scrollToEventIfNeeded(payload.event.getId());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case Action.ComposerInsert: {
|
||||||
|
// re-dispatch to the correct composer
|
||||||
|
if (this.state.editState) {
|
||||||
|
dis.dispatch({
|
||||||
|
...payload,
|
||||||
|
action: "edit_composer_insert",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
dis.dispatch({
|
||||||
|
...payload,
|
||||||
|
action: "send_composer_insert",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "scroll_to_bottom":
|
||||||
|
this.messagePanel?.jumpToLiveTimeline();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1267,7 +1295,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onSearch = (term: string, scope) => {
|
private onSearch = (term: string, scope: SearchScope) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
searchTerm: term,
|
searchTerm: term,
|
||||||
searchScope: scope,
|
searchScope: scope,
|
||||||
|
@ -1288,7 +1316,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
this.searchId = new Date().getTime();
|
this.searchId = new Date().getTime();
|
||||||
|
|
||||||
let roomId;
|
let roomId;
|
||||||
if (scope === "Room") roomId = this.state.room.roomId;
|
if (scope === SearchScope.Room) roomId = this.state.room.roomId;
|
||||||
|
|
||||||
debuglog("sending search request");
|
debuglog("sending search request");
|
||||||
const searchPromise = eventSearch(term, roomId);
|
const searchPromise = eventSearch(term, roomId);
|
||||||
|
@ -2043,6 +2071,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
showReactions={true}
|
showReactions={true}
|
||||||
layout={this.state.layout}
|
layout={this.state.layout}
|
||||||
|
editState={this.state.editState}
|
||||||
/>);
|
/>);
|
||||||
|
|
||||||
let topUnreadMessagesBar = null;
|
let topUnreadMessagesBar = null;
|
||||||
|
@ -2058,7 +2087,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
|
||||||
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
|
||||||
jumpToBottom = (<JumpToBottomButton
|
jumpToBottom = (<JumpToBottomButton
|
||||||
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0}
|
highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
|
||||||
numUnreadMessages={this.state.numUnreadMessages}
|
numUnreadMessages={this.state.numUnreadMessages}
|
||||||
onScrollToBottomClick={this.jumpToLiveTimeline}
|
onScrollToBottomClick={this.jumpToLiveTimeline}
|
||||||
roomId={this.state.roomId}
|
roomId={this.state.roomId}
|
||||||
|
|
|
@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {ReactNode, useMemo, useState} from "react";
|
import React, { ReactNode, useMemo, useState } from "react";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event";
|
import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import {sortBy} from "lodash";
|
import { sortBy } from "lodash";
|
||||||
|
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
import {_t} from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton";
|
import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
|
||||||
import BaseDialog from "../views/dialogs/BaseDialog";
|
import BaseDialog from "../views/dialogs/BaseDialog";
|
||||||
import Spinner from "../views/elements/Spinner";
|
import Spinner from "../views/elements/Spinner";
|
||||||
import SearchBox from "./SearchBox";
|
import SearchBox from "./SearchBox";
|
||||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import {useAsyncMemo} from "../../hooks/useAsyncMemo";
|
import { useAsyncMemo } from "../../hooks/useAsyncMemo";
|
||||||
import {EnhancedMap} from "../../utils/maps";
|
import { EnhancedMap } from "../../utils/maps";
|
||||||
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
import StyledCheckbox from "../views/elements/StyledCheckbox";
|
||||||
import AutoHideScrollbar from "./AutoHideScrollbar";
|
import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import BaseAvatar from "../views/avatars/BaseAvatar";
|
import BaseAvatar from "../views/avatars/BaseAvatar";
|
||||||
import {mediaFromMxc} from "../../customisations/Media";
|
import { mediaFromMxc } from "../../customisations/Media";
|
||||||
import InfoTooltip from "../views/elements/InfoTooltip";
|
import InfoTooltip from "../views/elements/InfoTooltip";
|
||||||
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
import TextWithTooltip from "../views/elements/TextWithTooltip";
|
||||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||||
import {getOrder} from "../../stores/SpaceStore";
|
import { getChildOrder } from "../../stores/SpaceStore";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import {linkifyElement} from "../../HtmlUtils";
|
import { linkifyElement } from "../../HtmlUtils";
|
||||||
|
|
||||||
interface IHierarchyProps {
|
interface IHierarchyProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -286,7 +286,7 @@ export const HierarchyLevel = ({
|
||||||
const children = Array.from(relations.get(spaceId)?.values() || []);
|
const children = Array.from(relations.get(spaceId)?.values() || []);
|
||||||
const sortedChildren = sortBy(children, ev => {
|
const sortedChildren = sortBy(children, ev => {
|
||||||
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
|
||||||
return getOrder(ev.content.order, null, ev.state_key);
|
return getChildOrder(ev.content.order, null, ev.state_key);
|
||||||
});
|
});
|
||||||
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
|
||||||
const roomId = ev.state_key;
|
const roomId = ev.state_key;
|
||||||
|
|
|
@ -14,58 +14,59 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {RefObject, useContext, useRef, useState} from "react";
|
import React, { RefObject, useContext, useRef, useState } from "react";
|
||||||
import {EventType} from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials";
|
||||||
import {EventSubscription} from "fbemitter";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
|
import { EventSubscription } from "fbemitter";
|
||||||
|
|
||||||
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../contexts/MatrixClientContext";
|
||||||
import RoomAvatar from "../views/avatars/RoomAvatar";
|
import RoomAvatar from "../views/avatars/RoomAvatar";
|
||||||
import {_t} from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import AccessibleButton from "../views/elements/AccessibleButton";
|
import AccessibleButton from "../views/elements/AccessibleButton";
|
||||||
import RoomName from "../views/elements/RoomName";
|
import RoomName from "../views/elements/RoomName";
|
||||||
import RoomTopic from "../views/elements/RoomTopic";
|
import RoomTopic from "../views/elements/RoomTopic";
|
||||||
import InlineSpinner from "../views/elements/InlineSpinner";
|
import InlineSpinner from "../views/elements/InlineSpinner";
|
||||||
import {inviteMultipleToRoom, showRoomInviteDialog} from "../../RoomInvite";
|
import { inviteMultipleToRoom, showRoomInviteDialog } from "../../RoomInvite";
|
||||||
import {useRoomMembers} from "../../hooks/useRoomMembers";
|
import { useRoomMembers } from "../../hooks/useRoomMembers";
|
||||||
import createRoom, {IOpts} from "../../createRoom";
|
import createRoom, { IOpts } from "../../createRoom";
|
||||||
import Field from "../views/elements/Field";
|
import Field from "../views/elements/Field";
|
||||||
import {useEventEmitter} from "../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../hooks/useEventEmitter";
|
||||||
import withValidation from "../views/elements/Validation";
|
import withValidation from "../views/elements/Validation";
|
||||||
import * as Email from "../../email";
|
import * as Email from "../../email";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../dispatcher/actions";
|
import dis from "../../dispatcher/dispatcher";
|
||||||
|
import { Action } from "../../dispatcher/actions";
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier"
|
import ResizeNotifier from "../../utils/ResizeNotifier"
|
||||||
import MainSplit from './MainSplit';
|
import MainSplit from './MainSplit';
|
||||||
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
import ErrorBoundary from "../views/elements/ErrorBoundary";
|
||||||
import {ActionPayload} from "../../dispatcher/payloads";
|
import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import RightPanel from "./RightPanel";
|
import RightPanel from "./RightPanel";
|
||||||
import RightPanelStore from "../../stores/RightPanelStore";
|
import RightPanelStore from "../../stores/RightPanelStore";
|
||||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
import {SetRightPanelPhasePayload} from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
import { SetRightPanelPhasePayload } from "../../dispatcher/payloads/SetRightPanelPhasePayload";
|
||||||
import {useStateArray} from "../../hooks/useStateArray";
|
import { useStateArray } from "../../hooks/useStateArray";
|
||||||
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
import SpacePublicShare from "../views/spaces/SpacePublicShare";
|
||||||
import {showAddExistingRooms, showCreateNewRoom, shouldShowSpaceSettings, showSpaceSettings} from "../../utils/space";
|
import { shouldShowSpaceSettings, showAddExistingRooms, showCreateNewRoom, showSpaceSettings } from "../../utils/space";
|
||||||
import {showRoom, SpaceHierarchy} from "./SpaceRoomDirectory";
|
import { showRoom, SpaceHierarchy } from "./SpaceRoomDirectory";
|
||||||
import MemberAvatar from "../views/avatars/MemberAvatar";
|
import MemberAvatar from "../views/avatars/MemberAvatar";
|
||||||
import {useStateToggle} from "../../hooks/useStateToggle";
|
import { useStateToggle } from "../../hooks/useStateToggle";
|
||||||
import SpaceStore from "../../stores/SpaceStore";
|
import SpaceStore from "../../stores/SpaceStore";
|
||||||
import FacePile from "../views/elements/FacePile";
|
import FacePile from "../views/elements/FacePile";
|
||||||
import {AddExistingToSpace} from "../views/dialogs/AddExistingToSpaceDialog";
|
import { AddExistingToSpace } from "../views/dialogs/AddExistingToSpaceDialog";
|
||||||
import {ChevronFace, ContextMenuButton, useContextMenu} from "./ContextMenu";
|
import { ChevronFace, ContextMenuButton, useContextMenu } from "./ContextMenu";
|
||||||
import IconizedContextMenu, {
|
import IconizedContextMenu, {
|
||||||
IconizedContextMenuOption,
|
IconizedContextMenuOption,
|
||||||
IconizedContextMenuOptionList,
|
IconizedContextMenuOptionList,
|
||||||
} from "../views/context_menus/IconizedContextMenu";
|
} from "../views/context_menus/IconizedContextMenu";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
import {BetaPill} from "../views/beta/BetaCard";
|
import { BetaPill } from "../views/beta/BetaCard";
|
||||||
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog";
|
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import dis from "../../dispatcher/dispatcher";
|
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
import BetaFeedbackDialog from "../views/dialogs/BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../SdkConfig";
|
import SdkConfig from "../../SdkConfig";
|
||||||
import { Preset } from "matrix-js-sdk/src/@types/partials";
|
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
space: Room;
|
space: Room;
|
||||||
|
@ -166,7 +167,7 @@ const SpaceInfo = ({ space }) => {
|
||||||
const onBetaClick = () => {
|
const onBetaClick = () => {
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: USER_LABS_TAB,
|
initialTabId: UserTab.Labs,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -178,6 +179,9 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
|
|
||||||
const spacesEnabled = SettingsStore.getValue("feature_spaces");
|
const spacesEnabled = SettingsStore.getValue("feature_spaces");
|
||||||
|
|
||||||
|
const cannotJoin = getEffectiveMembership(myMembership) === EffectiveMembership.Leave
|
||||||
|
&& space.getJoinRule() !== JoinRule.Public;
|
||||||
|
|
||||||
let inviterSection;
|
let inviterSection;
|
||||||
let joinButtons;
|
let joinButtons;
|
||||||
if (myMembership === "join") {
|
if (myMembership === "join") {
|
||||||
|
@ -244,7 +248,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
onJoinButtonClicked();
|
onJoinButtonClicked();
|
||||||
}}
|
}}
|
||||||
disabled={!spacesEnabled}
|
disabled={!spacesEnabled || cannotJoin}
|
||||||
>
|
>
|
||||||
{ _t("Join") }
|
{ _t("Join") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
@ -255,6 +259,30 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
joinButtons = <InlineSpinner />;
|
joinButtons = <InlineSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let footer;
|
||||||
|
if (!spacesEnabled) {
|
||||||
|
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||||
|
{ myMembership === "join"
|
||||||
|
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||||
|
spaceName: space.name,
|
||||||
|
}, {
|
||||||
|
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
|
})
|
||||||
|
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
||||||
|
spaceName: space.name,
|
||||||
|
}, {
|
||||||
|
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>;
|
||||||
|
} else if (cannotJoin) {
|
||||||
|
footer = <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
||||||
|
{ _t("To view %(spaceName)s, you need an invite", {
|
||||||
|
spaceName: space.name,
|
||||||
|
}) }
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_SpaceRoomView_preview">
|
return <div className="mx_SpaceRoomView_preview">
|
||||||
<BetaPill onClick={onBetaClick} />
|
<BetaPill onClick={onBetaClick} />
|
||||||
{ inviterSection }
|
{ inviterSection }
|
||||||
|
@ -274,20 +302,7 @@ const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
|
||||||
<div className="mx_SpaceRoomView_preview_joinButtons">
|
<div className="mx_SpaceRoomView_preview_joinButtons">
|
||||||
{ joinButtons }
|
{ joinButtons }
|
||||||
</div>
|
</div>
|
||||||
{ !spacesEnabled && <div className="mx_SpaceRoomView_preview_spaceBetaPrompt">
|
{ footer }
|
||||||
{ myMembership === "join"
|
|
||||||
? _t("To view %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
|
||||||
spaceName: space.name,
|
|
||||||
}, {
|
|
||||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
|
||||||
})
|
|
||||||
: _t("To join %(spaceName)s, turn on the <a>Spaces beta</a>", {
|
|
||||||
spaceName: space.name,
|
|
||||||
}, {
|
|
||||||
a: sub => <AccessibleButton onClick={onBetaClick} kind="link">{ sub }</AccessibleButton>,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</div> }
|
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -34,12 +34,10 @@ import * as sdk from "../../index";
|
||||||
import { Key } from '../../Keyboard';
|
import { Key } from '../../Keyboard';
|
||||||
import Timer from '../../utils/Timer';
|
import Timer from '../../utils/Timer';
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
import EditorStateTransfer from '../../utils/EditorStateTransfer';
|
|
||||||
import { haveTileForEvent } from "../views/rooms/EventTile";
|
import { haveTileForEvent } from "../views/rooms/EventTile";
|
||||||
import { UIFeature } from "../../settings/UIFeature";
|
import { UIFeature } from "../../settings/UIFeature";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { arrayFastClone } from "../../utils/arrays";
|
import { arrayFastClone } from "../../utils/arrays";
|
||||||
import { Action } from "../../dispatcher/actions";
|
|
||||||
|
|
||||||
const PAGINATE_SIZE = 20;
|
const PAGINATE_SIZE = 20;
|
||||||
const INITIAL_SIZE = 20;
|
const INITIAL_SIZE = 20;
|
||||||
|
@ -72,6 +70,8 @@ class TimelinePanel extends React.Component {
|
||||||
manageReadReceipts: PropTypes.bool,
|
manageReadReceipts: PropTypes.bool,
|
||||||
sendReadReceiptOnLoad: PropTypes.bool,
|
sendReadReceiptOnLoad: PropTypes.bool,
|
||||||
manageReadMarkers: PropTypes.bool,
|
manageReadMarkers: PropTypes.bool,
|
||||||
|
// with this enabled it'll listen and react to Action.ComposerInsert and `edit_event`
|
||||||
|
manageComposerDispatches: PropTypes.bool,
|
||||||
|
|
||||||
// true to give the component a 'display: none' style.
|
// true to give the component a 'display: none' style.
|
||||||
hidden: PropTypes.bool,
|
hidden: PropTypes.bool,
|
||||||
|
@ -444,38 +444,6 @@ class TimelinePanel extends React.Component {
|
||||||
case "ignore_state_changed":
|
case "ignore_state_changed":
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "edit_event": {
|
|
||||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
|
||||||
this.setState({editState}, () => {
|
|
||||||
if (payload.event && this._messagePanel.current) {
|
|
||||||
this._messagePanel.current.scrollToEventIfNeeded(
|
|
||||||
payload.event.getId(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case Action.ComposerInsert: {
|
|
||||||
// re-dispatch to the correct composer
|
|
||||||
if (this.state.editState) {
|
|
||||||
dis.dispatch({
|
|
||||||
...payload,
|
|
||||||
action: "edit_composer_insert",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
dis.dispatch({
|
|
||||||
...payload,
|
|
||||||
action: "send_composer_insert",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case "scroll_to_bottom":
|
|
||||||
this.jumpToLiveTimeline();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -866,6 +834,12 @@ class TimelinePanel extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
scrollToEventIfNeeded = (eventId) => {
|
||||||
|
if (this._messagePanel.current) {
|
||||||
|
this._messagePanel.current.scrollToEventIfNeeded(eventId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
|
/* scroll to show the read-up-to marker. We put it 1/3 of the way down
|
||||||
* the container.
|
* the container.
|
||||||
*/
|
*/
|
||||||
|
@ -1473,7 +1447,7 @@ class TimelinePanel extends React.Component {
|
||||||
tileShape={this.props.tileShape}
|
tileShape={this.props.tileShape}
|
||||||
resizeNotifier={this.props.resizeNotifier}
|
resizeNotifier={this.props.resizeNotifier}
|
||||||
getRelationsForEvent={this.getRelationsForEvent}
|
getRelationsForEvent={this.getRelationsForEvent}
|
||||||
editState={this.state.editState}
|
editState={this.props.editState}
|
||||||
showReactions={this.props.showReactions}
|
showReactions={this.props.showReactions}
|
||||||
layout={this.props.layout}
|
layout={this.props.layout}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
|
|
|
@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { Action } from "../../dispatcher/actions";
|
import { Action } from "../../dispatcher/actions";
|
||||||
import { _t } from "../../languageHandler";
|
import { _t } from "../../languageHandler";
|
||||||
import { ContextMenuButton } from "./ContextMenu";
|
import { ContextMenuButton } from "./ContextMenu";
|
||||||
import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog";
|
import { UserTab } from "../views/dialogs/UserSettingsDialog";
|
||||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
|
@ -408,12 +408,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_UserMenu_iconBell"
|
iconClassName="mx_UserMenu_iconBell"
|
||||||
label={_t("Notification settings")}
|
label={_t("Notification settings")}
|
||||||
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)}
|
onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
|
||||||
/>
|
/>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_UserMenu_iconLock"
|
iconClassName="mx_UserMenu_iconLock"
|
||||||
label={_t("Security & privacy")}
|
label={_t("Security & privacy")}
|
||||||
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)}
|
onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
|
||||||
/>
|
/>
|
||||||
<IconizedContextMenuOption
|
<IconizedContextMenuOption
|
||||||
iconClassName="mx_UserMenu_iconSettings"
|
iconClassName="mx_UserMenu_iconSettings"
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default class ViewSource extends React.Component {
|
||||||
viewSourceContent() {
|
viewSourceContent() {
|
||||||
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
|
||||||
const isEncrypted = mxEvent.isEncrypted();
|
const isEncrypted = mxEvent.isEncrypted();
|
||||||
const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private
|
const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
|
||||||
const originalEventSource = mxEvent.event;
|
const originalEventSource = mxEvent.event;
|
||||||
|
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
|
|
|
@ -18,14 +18,7 @@ import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {
|
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||||
SetupEncryptionStore,
|
|
||||||
PHASE_LOADING,
|
|
||||||
PHASE_INTRO,
|
|
||||||
PHASE_BUSY,
|
|
||||||
PHASE_DONE,
|
|
||||||
PHASE_CONFIRM_SKIP,
|
|
||||||
} from '../../../stores/SetupEncryptionStore';
|
|
||||||
import SetupEncryptionBody from "./SetupEncryptionBody";
|
import SetupEncryptionBody from "./SetupEncryptionBody";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -61,18 +54,18 @@ export default class CompleteSecurity extends React.Component {
|
||||||
let icon;
|
let icon;
|
||||||
let title;
|
let title;
|
||||||
|
|
||||||
if (phase === PHASE_LOADING) {
|
if (phase === Phase.Loading) {
|
||||||
return null;
|
return null;
|
||||||
} else if (phase === PHASE_INTRO) {
|
} else if (phase === Phase.Intro) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||||
title = _t("Verify this login");
|
title = _t("Verify this login");
|
||||||
} else if (phase === PHASE_DONE) {
|
} else if (phase === Phase.Done) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
|
||||||
title = _t("Session verified");
|
title = _t("Session verified");
|
||||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
} else if (phase === Phase.ConfirmSkip) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||||
title = _t("Are you sure?");
|
title = _t("Are you sure?");
|
||||||
} else if (phase === PHASE_BUSY) {
|
} else if (phase === Phase.Busy) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
|
||||||
title = _t("Verify this login");
|
title = _t("Verify this login");
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -269,7 +269,7 @@ export default class Registration extends React.Component<IProps, IState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onUIAuthFinished = async (success, response, extra) => {
|
private onUIAuthFinished = async (success: boolean, response: any) => {
|
||||||
if (!success) {
|
if (!success) {
|
||||||
let msg = response.message || response.toString();
|
let msg = response.message || response.toString();
|
||||||
// can we give a better error message?
|
// can we give a better error message?
|
||||||
|
|
|
@ -21,15 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
|
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {
|
import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
|
||||||
SetupEncryptionStore,
|
|
||||||
PHASE_LOADING,
|
|
||||||
PHASE_INTRO,
|
|
||||||
PHASE_BUSY,
|
|
||||||
PHASE_DONE,
|
|
||||||
PHASE_CONFIRM_SKIP,
|
|
||||||
PHASE_FINISHED,
|
|
||||||
} from '../../../stores/SetupEncryptionStore';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
function keyHasPassphrase(keyInfo) {
|
function keyHasPassphrase(keyInfo) {
|
||||||
|
@ -63,7 +55,7 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
|
|
||||||
_onStoreUpdate = () => {
|
_onStoreUpdate = () => {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
if (store.phase === PHASE_FINISHED) {
|
if (store.phase === Phase.Finished) {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -136,7 +128,7 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
onClose={this.props.onFinished}
|
onClose={this.props.onFinished}
|
||||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||||
/>;
|
/>;
|
||||||
} else if (phase === PHASE_INTRO) {
|
} else if (phase === Phase.Intro) {
|
||||||
const store = SetupEncryptionStore.sharedInstance();
|
const store = SetupEncryptionStore.sharedInstance();
|
||||||
let recoveryKeyPrompt;
|
let recoveryKeyPrompt;
|
||||||
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
|
||||||
|
@ -174,7 +166,7 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (phase === PHASE_DONE) {
|
} else if (phase === Phase.Done) {
|
||||||
let message;
|
let message;
|
||||||
if (this.state.backupInfo) {
|
if (this.state.backupInfo) {
|
||||||
message = <p>{_t(
|
message = <p>{_t(
|
||||||
|
@ -200,7 +192,7 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (phase === PHASE_CONFIRM_SKIP) {
|
} else if (phase === Phase.ConfirmSkip) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
|
@ -224,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
|
} else if (phase === Phase.Busy || phase === Phase.Loading) {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
return <Spinner />;
|
return <Spinner />;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
|
||||||
import * as AvatarLogic from '../../../Avatar';
|
import * as AvatarLogic from '../../../Avatar';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import RoomContext from "../../../contexts/RoomContext";
|
import RoomContext from "../../../contexts/RoomContext";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import {toPx} from "../../../utils/units";
|
import { toPx } from "../../../utils/units";
|
||||||
import {ResizeMethod} from "../../../Avatar";
|
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
|
||||||
import BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import {ResizeMethod} from "../../../Avatar";
|
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
|
|
|
@ -16,14 +16,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
|
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
|
||||||
|
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
import dis from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import BaseAvatar from "./BaseAvatar";
|
import BaseAvatar from "./BaseAvatar";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import {ResizeMethod} from "../../../Avatar";
|
|
||||||
|
|
||||||
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
|
||||||
member: RoomMember;
|
member: RoomMember;
|
||||||
|
|
|
@ -13,17 +13,17 @@ 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 React, {ComponentProps} from 'react';
|
import React, { ComponentProps } from 'react';
|
||||||
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 BaseAvatar from './BaseAvatar';
|
import BaseAvatar from './BaseAvatar';
|
||||||
import ImageView from '../elements/ImageView';
|
import ImageView from '../elements/ImageView';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import * as Avatar from '../../../Avatar';
|
import * as Avatar from '../../../Avatar';
|
||||||
import {ResizeMethod} from "../../../Avatar";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
|
||||||
|
|
||||||
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
|
||||||
// Room may be left unset here, but if it is,
|
// Room may be left unset here, but if it is,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
|
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import SdkConfig from "../../../SdkConfig";
|
||||||
|
import SettingsFlag from "../elements/SettingsFlag";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
||||||
const info = SettingsStore.getBetaInfo(featureId);
|
const info = SettingsStore.getBetaInfo(featureId);
|
||||||
if (!info) return null; // Beta is invalid/disabled
|
if (!info) return null; // Beta is invalid/disabled
|
||||||
|
|
||||||
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info;
|
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
|
||||||
const value = SettingsStore.getValue(featureId);
|
const value = SettingsStore.getValue(featureId);
|
||||||
|
|
||||||
let feedbackButton;
|
let feedbackButton;
|
||||||
|
@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div className="mx_BetaCard">
|
return <div className="mx_BetaCard">
|
||||||
<div>
|
<div className="mx_BetaCard_columns">
|
||||||
<h3 className="mx_BetaCard_title">
|
|
||||||
{ titleOverride || _t(title) }
|
|
||||||
<BetaPill />
|
|
||||||
</h3>
|
|
||||||
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
|
||||||
<div>
|
<div>
|
||||||
{ feedbackButton }
|
<h3 className="mx_BetaCard_title">
|
||||||
<AccessibleButton
|
{ titleOverride || _t(title) }
|
||||||
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
<BetaPill />
|
||||||
kind={feedbackButton ? "primary_outline" : "primary"}
|
</h3>
|
||||||
>
|
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
|
||||||
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
<div className="mx_BetaCard_buttons">
|
||||||
</AccessibleButton>
|
{ feedbackButton }
|
||||||
|
<AccessibleButton
|
||||||
|
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
|
||||||
|
kind={feedbackButton ? "primary_outline" : "primary"}
|
||||||
|
>
|
||||||
|
{ value ? _t("Leave the beta") : _t("Join the beta") }
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
||||||
|
{ disclaimer(value) }
|
||||||
|
</div> }
|
||||||
</div>
|
</div>
|
||||||
{ disclaimer && <div className="mx_BetaCard_disclaimer">
|
<img src={image} alt="" />
|
||||||
{ disclaimer(value) }
|
|
||||||
</div> }
|
|
||||||
</div>
|
</div>
|
||||||
<img src={image} alt="" />
|
{ extraSettings && <div className="mx_BetaCard_relatedSettings">
|
||||||
|
{ extraSettings.map(key => (
|
||||||
|
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
|
||||||
|
)) }
|
||||||
|
</div> }
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
|
||||||
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
|
import Field from "../elements/Field";
|
||||||
import Dialpad from '../voip/DialPad';
|
import Dialpad from '../voip/DialPad';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
|
||||||
this.setState({value: this.state.value + digit});
|
this.setState({value: this.state.value + digit});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onChange = (ev) => {
|
||||||
|
this.setState({value: ev.target.value});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return <ContextMenu {...this.props}>
|
return <ContextMenu {...this.props}>
|
||||||
<div className="mx_DialPadContextMenu_header">
|
<div className="mx_DialPadContextMenu_header">
|
||||||
<div>
|
<div>
|
||||||
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div>
|
<Field className="mx_DialPadContextMenu_dialled"
|
||||||
|
value={this.state.value} autoFocus={true}
|
||||||
|
onChange={this.onChange}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_DialPadContextMenu_horizSep" />
|
<div className="mx_DialPadContextMenu_horizSep" />
|
||||||
<div className="mx_DialPadContextMenu_dialPad">
|
<div className="mx_DialPadContextMenu_dialPad">
|
||||||
|
|
|
@ -28,12 +28,11 @@ import Resend from '../../../Resend';
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
import { isUrlPermitted } from '../../../HtmlUtils';
|
import { isUrlPermitted } from '../../../HtmlUtils';
|
||||||
import { isContentActionable } from '../../../utils/EventUtils';
|
import { isContentActionable } from '../../../utils/EventUtils';
|
||||||
import { MenuItem } from "../../structures/ContextMenu";
|
import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from './IconizedContextMenu';
|
||||||
import { EventType } from "matrix-js-sdk/src/@types/event";
|
import { EventType } from "matrix-js-sdk/src/@types/event";
|
||||||
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
|
||||||
import ForwardDialog from "../dialogs/ForwardDialog";
|
import ForwardDialog from "../dialogs/ForwardDialog";
|
||||||
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
|
@ -180,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
pinnedIds.push(eventId);
|
pinnedIds.push(eventId);
|
||||||
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
|
||||||
event_ids: [
|
event_ids: [
|
||||||
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids,
|
...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
|
||||||
eventId,
|
eventId,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -201,7 +200,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onQuoteClick = () => {
|
onQuoteClick = () => {
|
||||||
dis.dispatch<ComposerInsertPayload>({
|
dis.dispatch({
|
||||||
action: Action.ComposerInsert,
|
action: Action.ComposerInsert,
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
});
|
});
|
||||||
|
@ -258,55 +257,68 @@ export default class MessageContextMenu extends React.Component {
|
||||||
let externalURLButton;
|
let externalURLButton;
|
||||||
let quoteButton;
|
let quoteButton;
|
||||||
let collapseReplyThread;
|
let collapseReplyThread;
|
||||||
|
let redactItemList;
|
||||||
|
|
||||||
// status is SENT before remote-echo, null after
|
// status is SENT before remote-echo, null after
|
||||||
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
const isSent = !eventStatus || eventStatus === EventStatus.SENT;
|
||||||
if (!mxEvent.isRedacted()) {
|
if (!mxEvent.isRedacted()) {
|
||||||
if (unsentReactionsCount !== 0) {
|
if (unsentReactionsCount !== 0) {
|
||||||
resendReactionsButton = (
|
resendReactionsButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onResendReactionsClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
iconClassName="mx_MessageContextMenu_iconResend"
|
||||||
</MenuItem>
|
label={ _t('Resend %(unsentCount)s reaction(s)', {unsentCount: unsentReactionsCount}) }
|
||||||
|
onClick={this.onResendReactionsClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSent && this.state.canRedact) {
|
if (isSent && this.state.canRedact) {
|
||||||
redactButton = (
|
redactButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onRedactClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Remove') }
|
iconClassName="mx_MessageContextMenu_iconRedact"
|
||||||
</MenuItem>
|
label={_t("Remove")}
|
||||||
|
onClick={this.onRedactClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isContentActionable(mxEvent)) {
|
if (isContentActionable(mxEvent)) {
|
||||||
forwardButton = (
|
forwardButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onForwardClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Forward Message') }
|
iconClassName="mx_MessageContextMenu_iconForward"
|
||||||
</MenuItem>
|
label={_t("Forward")}
|
||||||
|
onClick={this.onForwardClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.state.canPin) {
|
if (this.state.canPin) {
|
||||||
pinButton = (
|
pinButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onPinClick}>
|
<IconizedContextMenuOption
|
||||||
{ this._isPinned() ? _t('Unpin Message') : _t('Pin Message') }
|
iconClassName="mx_MessageContextMenu_iconPin"
|
||||||
</MenuItem>
|
label={ this._isPinned() ? _t('Unpin') : _t('Pin') }
|
||||||
|
onClick={this.onPinClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewSourceButton = (
|
const viewSourceButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onViewSourceClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('View Source') }
|
iconClassName="mx_MessageContextMenu_iconSource"
|
||||||
</MenuItem>
|
label={_t("View source")}
|
||||||
|
onClick={this.onViewSourceClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.props.eventTileOps) {
|
if (this.props.eventTileOps) {
|
||||||
if (this.props.eventTileOps.isWidgetHidden()) {
|
if (this.props.eventTileOps.isWidgetHidden()) {
|
||||||
unhidePreviewButton = (
|
unhidePreviewButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onUnhidePreviewClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Unhide Preview') }
|
iconClassName="mx_MessageContextMenu_iconUnhidePreview"
|
||||||
</MenuItem>
|
label={_t("Show preview")}
|
||||||
|
onClick={this.onUnhidePreviewClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -317,77 +329,97 @@ export default class MessageContextMenu extends React.Component {
|
||||||
}
|
}
|
||||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||||
const permalinkButton = (
|
const permalinkButton = (
|
||||||
<MenuItem
|
<IconizedContextMenuOption
|
||||||
element="a"
|
iconClassName="mx_MessageContextMenu_iconPermalink"
|
||||||
className="mx_MessageContextMenu_field"
|
|
||||||
onClick={this.onPermalinkClick}
|
onClick={this.onPermalinkClick}
|
||||||
|
label= {_t('Share')}
|
||||||
|
element="a"
|
||||||
href={permalink}
|
href={permalink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
/>
|
||||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
|
||||||
? _t('Share Permalink') : _t('Share Message') }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
if (this.props.eventTileOps) { // this event is rendered using TextualBody
|
||||||
quoteButton = (
|
quoteButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onQuoteClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Quote') }
|
iconClassName="mx_MessageContextMenu_iconQuote"
|
||||||
</MenuItem>
|
label={_t("Quote")}
|
||||||
|
onClick={this.onQuoteClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bridges can provide a 'external_url' to link back to the source.
|
// Bridges can provide a 'external_url' to link back to the source.
|
||||||
if (
|
if (typeof (mxEvent.event.content.external_url) === "string" &&
|
||||||
typeof(mxEvent.event.content.external_url) === "string" &&
|
|
||||||
isUrlPermitted(mxEvent.event.content.external_url)
|
isUrlPermitted(mxEvent.event.content.external_url)
|
||||||
) {
|
) {
|
||||||
externalURLButton = (
|
externalURLButton = (
|
||||||
<MenuItem
|
<IconizedContextMenuOption
|
||||||
|
iconClassName="mx_MessageContextMenu_iconLink"
|
||||||
|
onClick={this.closeMenu}
|
||||||
|
label={ _t('Source URL') }
|
||||||
element="a"
|
element="a"
|
||||||
className="mx_MessageContextMenu_field"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
onClick={this.closeMenu}
|
|
||||||
href={mxEvent.event.content.external_url}
|
href={mxEvent.event.content.external_url}
|
||||||
>
|
/>
|
||||||
{ _t('Source URL') }
|
|
||||||
</MenuItem>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.collapseReplyThread) {
|
if (this.props.collapseReplyThread) {
|
||||||
collapseReplyThread = (
|
collapseReplyThread = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onCollapseReplyThreadClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Collapse Reply Thread') }
|
iconClassName="mx_MessageContextMenu_iconCollapse"
|
||||||
</MenuItem>
|
label={_t("Collapse reply thread")}
|
||||||
|
onClick={this.onCollapseReplyThreadClick}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let reportEventButton;
|
let reportEventButton;
|
||||||
if (mxEvent.getSender() !== me) {
|
if (mxEvent.getSender() !== me) {
|
||||||
reportEventButton = (
|
reportEventButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field" onClick={this.onReportEventClick}>
|
<IconizedContextMenuOption
|
||||||
{ _t('Report Content') }
|
iconClassName="mx_MessageContextMenu_iconReport"
|
||||||
</MenuItem>
|
label={_t("Report")}
|
||||||
|
onClick={this.onReportEventClick}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonItemsList = (
|
||||||
|
<IconizedContextMenuOptionList>
|
||||||
|
{ quoteButton }
|
||||||
|
{ forwardButton }
|
||||||
|
{ pinButton }
|
||||||
|
{ permalinkButton }
|
||||||
|
{ reportEventButton }
|
||||||
|
{ externalURLButton }
|
||||||
|
{ unhidePreviewButton }
|
||||||
|
{ viewSourceButton }
|
||||||
|
{ resendReactionsButton }
|
||||||
|
{ collapseReplyThread }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (redactButton) {
|
||||||
|
redactItemList = (
|
||||||
|
<IconizedContextMenuOptionList red>
|
||||||
|
{ redactButton }
|
||||||
|
</IconizedContextMenuOptionList>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageContextMenu">
|
<IconizedContextMenu
|
||||||
{ resendReactionsButton }
|
{...this.props}
|
||||||
{ redactButton }
|
className="mx_MessageContextMenu"
|
||||||
{ forwardButton }
|
compact={true}
|
||||||
{ pinButton }
|
>
|
||||||
{ viewSourceButton }
|
{ commonItemsList }
|
||||||
{ unhidePreviewButton }
|
{ redactItemList }
|
||||||
{ permalinkButton }
|
</IconizedContextMenu>
|
||||||
{ quoteButton }
|
|
||||||
{ externalURLButton }
|
|
||||||
{ collapseReplyThread }
|
|
||||||
{ reportEventButton }
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions';
|
||||||
import {MenuItem} from "../../structures/ContextMenu";
|
import {MenuItem} from "../../structures/ContextMenu";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
|
||||||
|
|
||||||
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
@replaceableComponent("views.context_menus.TagTileContextMenu")
|
||||||
export default class TagTileContextMenu extends React.Component {
|
export default class TagTileContextMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
tag: PropTypes.string.isRequired,
|
tag: PropTypes.string.isRequired,
|
||||||
|
index: PropTypes.number.isRequired,
|
||||||
/* callback called when the menu is dismissed */
|
/* callback called when the menu is dismissed */
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
constructor() {
|
_onViewCommunityClick = () => {
|
||||||
super();
|
|
||||||
|
|
||||||
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
|
||||||
this._onRemoveClick = this._onRemoveClick.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_onViewCommunityClick() {
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_group',
|
action: 'view_group',
|
||||||
group_id: this.props.tag,
|
group_id: this.props.tag,
|
||||||
});
|
});
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
};
|
||||||
|
|
||||||
_onRemoveClick() {
|
_onRemoveClick = () => {
|
||||||
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
_onMoveUp = () => {
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
|
_onMoveDown = () => {
|
||||||
|
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
|
||||||
|
this.props.onFinished();
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
let moveUp;
|
||||||
|
let moveDown;
|
||||||
|
if (this.props.index > 0) {
|
||||||
|
moveUp = (
|
||||||
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
|
||||||
|
{ _t("Move up") }
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
|
||||||
|
moveDown = (
|
||||||
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
|
||||||
|
{ _t("Move down") }
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
|
||||||
{ _t('View Community') }
|
{ _t('View Community') }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" role="separator" /> : null }
|
||||||
|
{ moveUp }
|
||||||
|
{ moveDown }
|
||||||
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
<hr className="mx_TagTileContextMenu_separator" role="separator" />
|
||||||
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
|
||||||
{ _t('Hide') }
|
{ _t("Unpin") }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
|
||||||
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
import QueryMatcher from "../../../autocomplete/QueryMatcher";
|
||||||
|
import TruncatedList from "../elements/TruncatedList";
|
||||||
|
import EntityTile from "../rooms/EntityTile";
|
||||||
|
import BaseAvatar from "../avatars/BaseAvatar";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
matrixClient: MatrixClient;
|
matrixClient: MatrixClient;
|
||||||
|
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
setSelectedToAdd(new Set(selectedToAdd));
|
setSelectedToAdd(new Set(selectedToAdd));
|
||||||
} : null;
|
} : null;
|
||||||
|
|
||||||
|
const [truncateAt, setTruncateAt] = useState(20);
|
||||||
|
function overflowTile(overflowCount, totalCount) {
|
||||||
|
const text = _t("and %(count)s others...", { count: overflowCount });
|
||||||
|
return (
|
||||||
|
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
|
||||||
|
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
|
||||||
|
} name={text} presenceState="online" suppressOnHover={true}
|
||||||
|
onClick={() => setTruncateAt(totalCount)} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return <div className="mx_AddExistingToSpace">
|
return <div className="mx_AddExistingToSpace">
|
||||||
<SearchBox
|
<SearchBox
|
||||||
className="mx_textinput_icon mx_textinput_search"
|
className="mx_textinput_icon mx_textinput_search"
|
||||||
|
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
|
||||||
{ rooms.length > 0 ? (
|
{ rooms.length > 0 ? (
|
||||||
<div className="mx_AddExistingToSpace_section">
|
<div className="mx_AddExistingToSpace_section">
|
||||||
<h3>{ _t("Rooms") }</h3>
|
<h3>{ _t("Rooms") }</h3>
|
||||||
{ rooms.map(room => {
|
<TruncatedList
|
||||||
return <Entry
|
truncateAt={truncateAt}
|
||||||
key={room.roomId}
|
createOverflowElement={overflowTile}
|
||||||
room={room}
|
getChildren={(start, end) => rooms.slice(start, end).map(room =>
|
||||||
checked={selectedToAdd.has(room)}
|
<Entry
|
||||||
onChange={onChange ? (checked) => {
|
key={room.roomId}
|
||||||
onChange(checked, room);
|
room={room}
|
||||||
} : null}
|
checked={selectedToAdd.has(room)}
|
||||||
/>;
|
onChange={onChange ? (checked) => {
|
||||||
}) }
|
onChange(checked, room);
|
||||||
|
} : null}
|
||||||
|
/>,
|
||||||
|
)}
|
||||||
|
getChildCount={() => rooms.length}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : undefined }
|
) : undefined }
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { addressTypes, getAddressType } from '../../../UserAddress.js';
|
import { addressTypes, getAddressType } from '../../../UserAddress';
|
||||||
import GroupStore from '../../../stores/GroupStore';
|
import GroupStore from '../../../stores/GroupStore';
|
||||||
import * as Email from '../../../email';
|
import * as Email from '../../../email';
|
||||||
import IdentityAuthClient from '../../../IdentityAuthClient';
|
import IdentityAuthClient from '../../../IdentityAuthClient';
|
||||||
|
|
|
@ -15,39 +15,41 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {SettingLevel} from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
unknownProfileUsers: Array<{
|
||||||
|
userId: string;
|
||||||
|
errorText: string;
|
||||||
|
}>;
|
||||||
|
onInviteAnyways: () => void;
|
||||||
|
onGiveUp: () => void;
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
|
@replaceableComponent("views.dialogs.AskInviteAnywayDialog")
|
||||||
export default class AskInviteAnywayDialog extends React.Component {
|
export default class AskInviteAnywayDialog extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private onInviteClicked = (): void => {
|
||||||
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
|
|
||||||
onInviteAnyways: PropTypes.func.isRequired,
|
|
||||||
onGiveUp: PropTypes.func.isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
_onInviteClicked = () => {
|
|
||||||
this.props.onInviteAnyways();
|
this.props.onInviteAnyways();
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onInviteNeverWarnClicked = () => {
|
private onInviteNeverWarnClicked = (): void => {
|
||||||
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
|
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
|
||||||
this.props.onInviteAnyways();
|
this.props.onInviteAnyways();
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGiveUpClicked = () => {
|
private onGiveUpClicked = (): void => {
|
||||||
this.props.onGiveUp();
|
this.props.onGiveUp();
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
const errorList = this.props.unknownProfileUsers
|
const errorList = this.props.unknownProfileUsers
|
||||||
|
@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className='mx_RetryInvitesDialog'
|
<BaseDialog className='mx_RetryInvitesDialog'
|
||||||
onFinished={this._onGiveUpClicked}
|
onFinished={this.onGiveUpClicked}
|
||||||
title={_t('The following users may not exist')}
|
title={_t('The following users may not exist')}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
<div id='mx_Dialog_content'>
|
<div id='mx_Dialog_content'>
|
||||||
|
{/* eslint-disable-next-line */}
|
||||||
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
|
||||||
<ul>
|
<ul>
|
||||||
{ errorList }
|
{ errorList }
|
||||||
|
@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<button onClick={this._onGiveUpClicked}>
|
<button onClick={this.onGiveUpClicked}>
|
||||||
{ _t('Close') }
|
{ _t('Close') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onInviteNeverWarnClicked}>
|
<button onClick={this.onInviteNeverWarnClicked}>
|
||||||
{ _t('Invite anyway and never warn me again') }
|
{ _t('Invite anyway and never warn me again') }
|
||||||
</button>
|
</button>
|
||||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
<button onClick={this.onInviteClicked} autoFocus={true}>
|
||||||
{ _t('Invite anyway') }
|
{ _t('Invite anyway') }
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
|
@ -29,7 +29,7 @@ import InfoDialog from "./InfoDialog";
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../../dispatcher/dispatcher";
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import {Action} from "../../../dispatcher/actions";
|
||||||
import {USER_LABS_TAB} from "./UserSettingsDialog";
|
import { UserTab } from "./UserSettingsDialog";
|
||||||
|
|
||||||
interface IProps extends IDialogProps {
|
interface IProps extends IDialogProps {
|
||||||
featureId: string;
|
featureId: string;
|
||||||
|
@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
|
||||||
const sendFeedback = async (ok: boolean) => {
|
const sendFeedback = async (ok: boolean) => {
|
||||||
if (!ok) return onFinished(false);
|
if (!ok) return onFinished(false);
|
||||||
|
|
||||||
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact);
|
const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
|
||||||
|
o[k] = SettingsStore.getValue(k);
|
||||||
|
return o;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
|
||||||
onFinished(true);
|
onFinished(true);
|
||||||
|
|
||||||
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
|
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
|
||||||
|
@ -70,7 +75,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
|
||||||
onFinished(false);
|
onFinished(false);
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: USER_LABS_TAB,
|
initialTabId: UserTab.Labs,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
{ _t("To leave the beta, visit your settings.") }
|
{ _t("To leave the beta, visit your settings.") }
|
||||||
|
|
|
@ -18,7 +18,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh
|
||||||
import AccessibleButton from "../elements/AccessibleButton";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
initialText?: string;
|
||||||
|
label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
sendLogs: boolean;
|
||||||
|
busy: boolean;
|
||||||
|
err: string;
|
||||||
|
issueUrl: string;
|
||||||
|
text: string;
|
||||||
|
progress: string;
|
||||||
|
downloadBusy: boolean;
|
||||||
|
downloadProgress: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.BugReportDialog")
|
@replaceableComponent("views.dialogs.BugReportDialog")
|
||||||
export default class BugReportDialog extends React.Component {
|
export default class BugReportDialog extends React.Component<IProps, IState> {
|
||||||
|
private unmounted: boolean;
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component {
|
||||||
downloadBusy: false,
|
downloadBusy: false,
|
||||||
downloadProgress: null,
|
downloadProgress: null,
|
||||||
};
|
};
|
||||||
this._unmounted = false;
|
this.unmounted = false;
|
||||||
this._onSubmit = this._onSubmit.bind(this);
|
|
||||||
this._onCancel = this._onCancel.bind(this);
|
|
||||||
this._onTextChange = this._onTextChange.bind(this);
|
|
||||||
this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
|
|
||||||
this._onSendLogsChange = this._onSendLogsChange.bind(this);
|
|
||||||
this._sendProgressCallback = this._sendProgressCallback.bind(this);
|
|
||||||
this._downloadProgressCallback = this._downloadProgressCallback.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this._unmounted = true;
|
this.unmounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancel(ev) {
|
private onCancel = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSubmit(ev) {
|
private onSubmit = (): void => {
|
||||||
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
|
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
|
||||||
this.setState({
|
this.setState({
|
||||||
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
|
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
|
||||||
|
@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component {
|
||||||
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
|
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
|
||||||
|
|
||||||
this.setState({ busy: true, progress: null, err: null });
|
this.setState({ busy: true, progress: null, err: null });
|
||||||
this._sendProgressCallback(_t("Preparing to send logs"));
|
this.sendProgressCallback(_t("Preparing to send logs"));
|
||||||
|
|
||||||
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
|
||||||
userText,
|
userText,
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
progressCallback: this._sendProgressCallback,
|
progressCallback: this.sendProgressCallback,
|
||||||
label: this.props.label,
|
label: this.props.label,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
if (!this._unmounted) {
|
if (!this.unmounted) {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
// N.B. first param is passed to piwik and so doesn't want i18n
|
// N.B. first param is passed to piwik and so doesn't want i18n
|
||||||
|
@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
if (!this._unmounted) {
|
if (!this.unmounted) {
|
||||||
this.setState({
|
this.setState({
|
||||||
busy: false,
|
busy: false,
|
||||||
progress: null,
|
progress: null,
|
||||||
|
@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDownload = async (ev) => {
|
private onDownload = async (): Promise<void> => {
|
||||||
this.setState({ downloadBusy: true });
|
this.setState({ downloadBusy: true });
|
||||||
this._downloadProgressCallback(_t("Preparing to download logs"));
|
this.downloadProgressCallback(_t("Preparing to download logs"));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadBugReport({
|
await downloadBugReport({
|
||||||
sendLogs: true,
|
sendLogs: true,
|
||||||
progressCallback: this._downloadProgressCallback,
|
progressCallback: this.downloadProgressCallback,
|
||||||
label: this.props.label,
|
label: this.props.label,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
downloadProgress: null,
|
downloadProgress: null,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!this._unmounted) {
|
if (!this.unmounted) {
|
||||||
this.setState({
|
this.setState({
|
||||||
downloadBusy: false,
|
downloadBusy: false,
|
||||||
downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
|
downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
|
||||||
|
@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_onTextChange(ev) {
|
private onTextChange = (ev: React.FormEvent<HTMLTextAreaElement>): void => {
|
||||||
this.setState({ text: ev.target.value });
|
this.setState({ text: ev.currentTarget.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onIssueUrlChange(ev) {
|
private onIssueUrlChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ issueUrl: ev.target.value });
|
this.setState({ issueUrl: ev.currentTarget.value });
|
||||||
}
|
}
|
||||||
|
|
||||||
_onSendLogsChange(ev) {
|
private sendProgressCallback = (progress: string): void => {
|
||||||
this.setState({ sendLogs: ev.target.checked });
|
if (this.unmounted) {
|
||||||
}
|
|
||||||
|
|
||||||
_sendProgressCallback(progress) {
|
|
||||||
if (this._unmounted) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({progress: progress});
|
this.setState({ progress });
|
||||||
}
|
}
|
||||||
|
|
||||||
_downloadProgressCallback(downloadProgress) {
|
private downloadProgressCallback = (downloadProgress: string): void => {
|
||||||
if (this._unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({ downloadProgress });
|
this.setState({ downloadProgress });
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const Loader = sdk.getComponent("elements.Spinner");
|
const Loader = sdk.getComponent("elements.Spinner");
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel}
|
<BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel}
|
||||||
title={_t('Submit debug logs')}
|
title={_t('Submit debug logs')}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
>
|
>
|
||||||
|
@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
</b></p>
|
</b></p>
|
||||||
|
|
||||||
<div className="mx_BugReportDialog_download">
|
<div className="mx_BugReportDialog_download">
|
||||||
<AccessibleButton onClick={this._onDownload} kind="link" disabled={this.state.downloadBusy}>
|
<AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
|
||||||
{ _t("Download logs") }
|
{ _t("Download logs") }
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
|
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
|
||||||
|
@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
type="text"
|
type="text"
|
||||||
className="mx_BugReportDialog_field_input"
|
className="mx_BugReportDialog_field_input"
|
||||||
label={_t("GitHub issue")}
|
label={_t("GitHub issue")}
|
||||||
onChange={this._onIssueUrlChange}
|
onChange={this.onIssueUrlChange}
|
||||||
value={this.state.issueUrl}
|
value={this.state.issueUrl}
|
||||||
placeholder="https://github.com/vector-im/element-web/issues/..."
|
placeholder="https://github.com/vector-im/element-web/issues/..."
|
||||||
/>
|
/>
|
||||||
|
@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component {
|
||||||
element="textarea"
|
element="textarea"
|
||||||
label={_t("Notes")}
|
label={_t("Notes")}
|
||||||
rows={5}
|
rows={5}
|
||||||
onChange={this._onTextChange}
|
onChange={this.onTextChange}
|
||||||
value={this.state.text}
|
value={this.state.text}
|
||||||
placeholder={_t(
|
placeholder={_t(
|
||||||
"If there is additional context that would help in " +
|
"If there is additional context that would help in " +
|
||||||
|
@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component {
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={_t("Send logs")}
|
<DialogButtons primaryButton={_t("Send logs")}
|
||||||
onPrimaryButtonClick={this._onSubmit}
|
onPrimaryButtonClick={this.onSubmit}
|
||||||
focus={true}
|
focus={true}
|
||||||
onCancel={this._onCancel}
|
onCancel={this.onCancel}
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BugReportDialog.propTypes = {
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
initialText: PropTypes.string,
|
|
||||||
};
|
|
|
@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import request from 'browser-request';
|
import request from 'browser-request';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
newVersion: string;
|
||||||
|
version: string;
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
|
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
|
||||||
|
|
||||||
export default class ChangelogDialog extends React.Component {
|
export default class ChangelogDialog extends React.Component<IProps> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {};
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount() {
|
||||||
const version = this.props.newVersion.split('-');
|
const version = this.props.newVersion.split('-');
|
||||||
const version2 = this.props.version.split('-');
|
const version2 = this.props.version.split('-');
|
||||||
if (version == null || version2 == null) return;
|
if (version == null || version2 == null) return;
|
||||||
|
@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_elementsForCommit(commit) {
|
private elementsForCommit(commit): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||||
|
@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
|
||||||
|
|
||||||
|
@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component {
|
||||||
msg: this.state[repo],
|
msg: this.state[repo],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
content = this.state[repo].map(this._elementsForCommit);
|
content = this.state[repo].map(this.elementsForCommit);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div key={repo}>
|
<div key={repo}>
|
||||||
|
@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ChangelogDialog.propTypes = {
|
|
||||||
version: PropTypes.string.isRequired,
|
|
||||||
newVersion: PropTypes.string.isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|