Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into export-conversations
This commit is contained in:
commit
45be4a6d02
98 changed files with 1679 additions and 975 deletions
3
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
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 -->
|
13
.github/workflows/develop.yml
vendored
13
.github/workflows/develop.yml
vendored
|
@ -1,4 +1,4 @@
|
||||||
name: Develop jobs
|
name: Develop
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [develop]
|
branches: [develop]
|
||||||
|
@ -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/**/*
|
||||||
|
@ -31,9 +34,7 @@ jobs:
|
||||||
tool: 'jsperformanceentry'
|
tool: 'jsperformanceentry'
|
||||||
output-file-path: test/end-to-end-tests/performance-entries.json
|
output-file-path: test/end-to-end-tests/performance-entries.json
|
||||||
fail-on-alert: false
|
fail-on-alert: false
|
||||||
# Secrets are not passed to fork, the action won't be able to comment
|
comment-on-alert: false
|
||||||
# for community PRs
|
|
||||||
comment-on-alert: ${{ github.repository_owner == 'matrix-org' }}
|
|
||||||
# Only temporary to monitor where failures occur
|
# Only temporary to monitor where failures occur
|
||||||
alert-comment-cc-users: '@gsouquet'
|
alert-comment-cc-users: '@gsouquet'
|
||||||
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
|
github-token: ${{ secrets.DEPLOY_GH_PAGES }}
|
||||||
|
|
130
CHANGELOG.md
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)
|
||||||
|
|
|
@ -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": {
|
||||||
|
@ -80,7 +80,7 @@
|
||||||
"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.14",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"opus-recorder": "^8.0.3",
|
"opus-recorder": "^8.0.3",
|
||||||
|
@ -143,12 +143,12 @@
|
||||||
"@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",
|
||||||
|
|
|
@ -98,5 +98,29 @@ limitations under the License.
|
||||||
line-height: $font-24px;
|
line-height: $font-24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallBox_iconButton {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
background-color: $icon-button-color;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-position: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallBox_silence::before {
|
||||||
|
mask-image: url('$(res)/img/voip/silence.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_IncomingCallBox_unSilence::before {
|
||||||
|
mask-image: url('$(res)/img/voip/un-silence.svg');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
3
res/img/voip/silence.svg
Normal file
3
res/img/voip/silence.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="M7.56986 1.82566L4 4.80054L1.5 4.80054C0.671573 4.80054 0 5.47212 0 6.30054V9.70054C0 10.529 0.671573 11.2005 1.5 11.2005L4 11.2005L7.56986 14.1754C8.05836 14.5825 8.8 14.2351 8.8 13.5993L8.8 9.70054V6.30054L8.8 2.40182C8.8 1.76595 8.05836 1.41858 7.56986 1.82566ZM14.1546 2.76877C13.9162 2.46224 13.4745 2.40702 13.1679 2.64543L13.0443 3.6318L13.0446 3.63212L13.0448 3.63238L13.0536 3.64417C13.0623 3.65582 13.0764 3.67498 13.0951 3.7013C13.1325 3.75399 13.1883 3.83518 13.2564 3.94222C13.3929 4.15668 13.5774 4.47271 13.7624 4.86922C14.1345 5.66647 14.4965 6.763 14.4965 8.00044C14.4965 9.23789 14.1345 10.3344 13.7624 11.1317C13.5774 11.5282 13.3929 11.8442 13.2564 12.0587C13.1883 12.1657 13.1325 12.2469 13.0951 12.2996C13.0764 12.3259 13.0623 12.3451 13.0536 12.3567L13.0448 12.3685L13.0446 12.3688L13.0443 12.3691L13.0441 12.3694L13.0438 12.3698L13.0436 12.37C12.8063 12.6765 12.8618 13.1174 13.1679 13.3555C13.4745 13.5939 13.9162 13.5386 14.1546 13.2321L13.5996 12.8004C14.1546 13.2321 14.1548 13.2319 14.1549 13.2317L14.1552 13.2313L14.156 13.2303L14.158 13.2278L14.1636 13.2204L14.1815 13.1966C14.1963 13.1768 14.2166 13.1491 14.2416 13.1138C14.2917 13.0433 14.3609 12.9423 14.4428 12.8136C14.6063 12.5567 14.8218 12.187 15.0368 11.7264C15.4647 10.8093 15.9027 9.50586 15.9027 8.00044C15.9027 6.49503 15.4647 5.19156 15.0368 4.27453C14.8218 3.8139 14.6063 3.44421 14.4428 3.18724C14.3609 3.05857 14.2917 2.95762 14.2416 2.88709C14.2166 2.8518 14.1963 2.82408 14.1815 2.80426L14.1636 2.78048L14.158 2.7731L14.156 2.77055L14.1552 2.76956L14.1549 2.76914C14.1548 2.76895 14.1546 2.76877 13.5996 3.20045L14.1546 2.76877ZM11.7552 5.16879C11.5168 4.86227 11.075 4.80705 10.7685 5.04546C10.4628 5.28321 10.4071 5.72319 10.6432 6.02961L10.6452 6.03231C10.6481 6.03609 10.6535 6.04353 10.6613 6.05445C10.6768 6.07633 10.7014 6.11199 10.732 6.1601C10.7935 6.2567 10.878 6.4013 10.963 6.58353C11.1351 6.95221 11.2971 7.44874 11.2971 8.00047C11.2971 8.5522 11.1351 9.04873 10.963 9.41741C10.878 9.59964 10.7935 9.74424 10.732 9.84084C10.7014 9.88895 10.6768 9.92461 10.6613 9.94648C10.6535 9.95741 10.6481 9.96484 10.6452 9.96863L10.6432 9.97132C10.4071 10.2777 10.4628 10.7177 10.7685 10.9555C11.075 11.1939 11.5168 11.1387 11.7552 10.8321L11.2002 10.4005C11.7552 10.8321 11.7553 10.832 11.7555 10.8318L11.7558 10.8314L11.7564 10.8305L11.758 10.8286L11.7619 10.8234L11.7731 10.8085C11.782 10.7966 11.7937 10.7806 11.8078 10.7607C11.8361 10.721 11.874 10.6656 11.9184 10.5958C12.0069 10.4567 12.1224 10.2584 12.2374 10.0121C12.4653 9.52364 12.7033 8.82017 12.7033 8.00047C12.7033 7.18077 12.4653 6.4773 12.2374 5.98884C12.1224 5.74249 12.0069 5.54424 11.9184 5.40512C11.874 5.33538 11.8361 5.27996 11.8078 5.24023C11.7937 5.22035 11.782 5.20435 11.7731 5.1924L11.7619 5.17752L11.758 5.17238L11.7564 5.17039L11.7558 5.16954L11.7555 5.16916C11.7553 5.16897 11.7552 5.16879 11.2002 5.60047L11.7552 5.16879Z" fill="#8D99A5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 3 KiB |
4
res/img/voip/un-silence.svg
Normal file
4
res/img/voip/un-silence.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M1.18262 0.960693L14.3815 14.1596" stroke="#8D99A5" stroke-width="1.61751" stroke-miterlimit="10" stroke-linecap="round"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.57061 4.20635L5.19539 4.51904H3.58059L10.2419 11.1804V8.87764L5.57061 4.20635ZM10.2419 6.59013V2.15546C10.2419 1.42405 9.38884 1.0245 8.82695 1.49274L6.81834 3.16658L10.2419 6.59013ZM5.19526 11.2479H2.7146C1.76172 11.2479 0.989258 10.4754 0.989258 9.52254V6.24438C0.989258 5.69051 1.25024 5.1976 1.65595 4.88191L10.2419 13.4679V13.6117C10.2419 14.3431 9.38884 14.7426 8.82695 14.2744L5.19526 11.248V11.2479Z" fill="#8D99A5"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 713 B |
|
@ -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 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
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,44 @@ 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,
|
||||||
|
# Then we check if BUILDKITE_BRANCH is defined,
|
||||||
# if it isn't we can assume this is a Netlify build
|
# if it isn't we can assume this is a Netlify build
|
||||||
if [ -z ${BUILDKITE_BRANCH+x} ]; then
|
if [ -n ${GITHUB_HEAD_REF+x} ]; then
|
||||||
|
head=$GITHUB_HEAD_REF
|
||||||
|
elif [ -n ${BUILDKITE_BRANCH+x} ]; then
|
||||||
|
head=$BUILDKITE_BRANCH
|
||||||
|
else
|
||||||
# Netlify doesn't give us info about the fork so we have to get it from GitHub API
|
# 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="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/"
|
||||||
apiEndpoint+=$REVIEW_ID
|
apiEndpoint+=$REVIEW_ID
|
||||||
head=$(curl $apiEndpoint | jq -r '.head.label')
|
head=$(curl $apiEndpoint | jq -r '.head.label')
|
||||||
else
|
|
||||||
head=$BUILDKITE_BRANCH
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# If head is set, it will contain either:
|
# If head is set, it will contain on BuilKite 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 [[ "$GITHUB_REPOSITORY" = "$deforg/$defrepo" ]]; then
|
||||||
|
clone $deforg $defrepo $head
|
||||||
|
else
|
||||||
|
clone $GITHUB_ACTOR $defrepo $head
|
||||||
|
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+x} ]; then
|
||||||
|
clone $deforg $defrepo $GITHUB_BASE_REF
|
||||||
|
elif [ -n ${BUILDKITE_PULL_REQUEST_BASE_BRANCH+x} ]; 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.
|
||||||
|
|
2
src/@types/global.d.ts
vendored
2
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 {
|
||||||
|
|
|
@ -14,18 +14,23 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 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";
|
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(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
export function avatarUrlForMember(
|
||||||
|
member: RoomMember,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
resizeMethod: ResizeMethod,
|
||||||
|
): string {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (member?.getMxcAvatarUrl()) {
|
if (member?.getMxcAvatarUrl()) {
|
||||||
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
|
@ -39,7 +44,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
export function avatarUrlForUser(
|
||||||
|
user: Pick<User, "avatarUrl">,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
resizeMethod?: ResizeMethod,
|
||||||
|
): string | null {
|
||||||
if (!user.avatarUrl) return null;
|
if (!user.avatarUrl) return null;
|
||||||
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
|
||||||
// (and store the ID of their native room)
|
// (and store the ID of their native room)
|
||||||
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
|
||||||
|
|
||||||
enum AudioID {
|
export enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
Ringback = 'ringbackAudio',
|
Ringback = 'ringbackAudio',
|
||||||
CallEnd = 'callendAudio',
|
CallEnd = 'callendAudio',
|
||||||
|
|
|
@ -468,7 +468,7 @@ function restoreEncryptionInfo(searchResultSlice = []) {
|
||||||
ev.event.curve25519Key,
|
ev.event.curve25519Key,
|
||||||
ev.event.ed25519Key,
|
ev.event.ed25519Key,
|
||||||
);
|
);
|
||||||
ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
|
ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
|
||||||
|
|
||||||
delete ev.event.curve25519Key;
|
delete ev.event.curve25519Key;
|
||||||
delete ev.event.ed25519Key;
|
delete ev.event.ed25519Key;
|
||||||
|
|
|
@ -150,6 +150,10 @@ function success(promise?: Promise<any>) {
|
||||||
return {promise};
|
return {promise};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function successSync(value: any) {
|
||||||
|
return success(Promise.resolve(value));
|
||||||
|
}
|
||||||
|
|
||||||
/* Disable the "unexpected this" error for these commands - all of the run
|
/* Disable the "unexpected this" error for these commands - all of the run
|
||||||
* functions are called with `this` bound to the Command instance.
|
* functions are called with `this` bound to the Command instance.
|
||||||
*/
|
*/
|
||||||
|
@ -160,7 +164,7 @@ export const Commands = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
description: _td('Sends the given message as a spoiler'),
|
description: _td('Sends the given message as a spoiler'),
|
||||||
runFn: function(roomId, message) {
|
runFn: function(roomId, message) {
|
||||||
return success(ContentHelpers.makeHtmlMessage(
|
return successSync(ContentHelpers.makeHtmlMessage(
|
||||||
message,
|
message,
|
||||||
`<span data-mx-spoiler>${message}</span>`,
|
`<span data-mx-spoiler>${message}</span>`,
|
||||||
));
|
));
|
||||||
|
@ -176,7 +180,7 @@ export const Commands = [
|
||||||
if (args) {
|
if (args) {
|
||||||
message = message + ' ' + args;
|
message = message + ' ' + args;
|
||||||
}
|
}
|
||||||
return success(ContentHelpers.makeTextMessage(message));
|
return successSync(ContentHelpers.makeTextMessage(message));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -189,7 +193,7 @@ export const Commands = [
|
||||||
if (args) {
|
if (args) {
|
||||||
message = message + ' ' + args;
|
message = message + ' ' + args;
|
||||||
}
|
}
|
||||||
return success(ContentHelpers.makeTextMessage(message));
|
return successSync(ContentHelpers.makeTextMessage(message));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -202,7 +206,7 @@ export const Commands = [
|
||||||
if (args) {
|
if (args) {
|
||||||
message = message + ' ' + args;
|
message = message + ' ' + args;
|
||||||
}
|
}
|
||||||
return success(ContentHelpers.makeTextMessage(message));
|
return successSync(ContentHelpers.makeTextMessage(message));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -215,7 +219,7 @@ export const Commands = [
|
||||||
if (args) {
|
if (args) {
|
||||||
message = message + ' ' + args;
|
message = message + ' ' + args;
|
||||||
}
|
}
|
||||||
return success(ContentHelpers.makeTextMessage(message));
|
return successSync(ContentHelpers.makeTextMessage(message));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -224,7 +228,7 @@ export const Commands = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
description: _td('Sends a message as plain text, without interpreting it as markdown'),
|
description: _td('Sends a message as plain text, without interpreting it as markdown'),
|
||||||
runFn: function(roomId, messages) {
|
runFn: function(roomId, messages) {
|
||||||
return success(ContentHelpers.makeTextMessage(messages));
|
return successSync(ContentHelpers.makeTextMessage(messages));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -233,7 +237,7 @@ export const Commands = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
description: _td('Sends a message as html, without interpreting it as markdown'),
|
description: _td('Sends a message as html, without interpreting it as markdown'),
|
||||||
runFn: function(roomId, messages) {
|
runFn: function(roomId, messages) {
|
||||||
return success(ContentHelpers.makeHtmlMessage(messages, messages));
|
return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -978,7 +982,7 @@ export const Commands = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
if (!args) return reject(this.getUserId());
|
if (!args) return reject(this.getUserId());
|
||||||
return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
|
return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
@ -988,7 +992,7 @@ export const Commands = [
|
||||||
args: '<message>',
|
args: '<message>',
|
||||||
runFn: function(roomId, args) {
|
runFn: function(roomId, args) {
|
||||||
if (!args) return reject(this.getUserId());
|
if (!args) return reject(this.getUserId());
|
||||||
return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
|
return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
|
||||||
},
|
},
|
||||||
category: CommandCategories.messages,
|
category: CommandCategories.messages,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -24,7 +24,9 @@ import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
// is sip virtual: there could be others in the future.
|
// is sip virtual: there could be others in the future.
|
||||||
|
|
||||||
export default class VoipUserMapper {
|
export default class VoipUserMapper {
|
||||||
private virtualRoomIdCache = new Set<string>();
|
// We store mappings of virtual -> native room IDs here until the local echo for the
|
||||||
|
// account data arrives.
|
||||||
|
private virtualToNativeRoomIdCache = new Map<string, string>();
|
||||||
|
|
||||||
public static sharedInstance(): VoipUserMapper {
|
public static sharedInstance(): VoipUserMapper {
|
||||||
if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
|
if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
|
||||||
|
@ -49,10 +51,20 @@ export default class VoipUserMapper {
|
||||||
native_room: roomId,
|
native_room: roomId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.virtualToNativeRoomIdCache.set(virtualRoomId, roomId);
|
||||||
|
|
||||||
return virtualRoomId;
|
return virtualRoomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public nativeRoomForVirtualRoom(roomId: string): string {
|
public nativeRoomForVirtualRoom(roomId: string): string {
|
||||||
|
const cachedNativeRoomId = this.virtualToNativeRoomIdCache.get(roomId);
|
||||||
|
if (cachedNativeRoomId) {
|
||||||
|
console.log(
|
||||||
|
"Returning native room ID " + cachedNativeRoomId + " for virtual room ID " + roomId + " from cache",
|
||||||
|
);
|
||||||
|
return cachedNativeRoomId;
|
||||||
|
}
|
||||||
|
|
||||||
const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
|
const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
|
||||||
if (!virtualRoom) return null;
|
if (!virtualRoom) return null;
|
||||||
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
|
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
|
||||||
|
@ -67,7 +79,7 @@ export default class VoipUserMapper {
|
||||||
public isVirtualRoom(room: Room): boolean {
|
public isVirtualRoom(room: Room): boolean {
|
||||||
if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
|
if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
|
||||||
|
|
||||||
if (this.virtualRoomIdCache.has(room.roomId)) return true;
|
if (this.virtualToNativeRoomIdCache.has(room.roomId)) return true;
|
||||||
|
|
||||||
// also look in the create event for the claimed native room ID, which is the only
|
// also look in the create event for the claimed native room ID, which is the only
|
||||||
// way we can recognise a virtual room we've created when it first arrives down
|
// way we can recognise a virtual room we've created when it first arrives down
|
||||||
|
@ -110,7 +122,7 @@ export default class VoipUserMapper {
|
||||||
|
|
||||||
// also put this room in the virtual room ID cache so isVirtualRoom return the right answer
|
// also put this room in the virtual room ID cache so isVirtualRoom return the right answer
|
||||||
// in however long it takes for the echo of setAccountData to come down the sync
|
// in however long it takes for the echo of setAccountData to come down the sync
|
||||||
this.virtualRoomIdCache.add(invitedRoom.roomId);
|
this.virtualToNativeRoomIdCache.set(invitedRoom.roomId, nativeRoom.roomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,13 +32,9 @@ 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);
|
||||||
|
|
|
@ -20,19 +20,19 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { _t } from '../languageHandler';
|
import { _t } from '../languageHandler';
|
||||||
import AutocompleteProvider from './AutocompleteProvider';
|
import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {PillCompletion} from './Components';
|
import { PillCompletion } from './Components';
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {sortBy} from 'lodash';
|
import { sortBy } from 'lodash';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
|
||||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import Room from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import RoomMember from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import RoomState from "matrix-js-sdk/src/models/room-state";
|
import { RoomState } from "matrix-js-sdk/src/models/room-state";
|
||||||
import EventTimeline from "matrix-js-sdk/src/models/event-timeline";
|
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import {makeUserPermalink} from "../utils/permalinks/Permalinks";
|
import { makeUserPermalink } from "../utils/permalinks/Permalinks";
|
||||||
import {ICompletion, ISelectionRange} from "./Autocompleter";
|
import { ICompletion, ISelectionRange } from "./Autocompleter";
|
||||||
|
|
||||||
const USER_REGEX = /\B@\S*/g;
|
const USER_REGEX = /\B@\S*/g;
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ export function getUnsentMessages(room) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("structures.RoomStatusBar")
|
@replaceableComponent("structures.RoomStatusBar")
|
||||||
export default class RoomStatusBar extends React.Component {
|
export default class RoomStatusBar extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// the room this statusbar is representing.
|
// the room this statusbar is representing.
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -25,6 +25,7 @@ import React, { createRef } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { 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 { EventSubscription } from "fbemitter";
|
import { EventSubscription } from "fbemitter";
|
||||||
|
|
||||||
import shouldHideEvent from '../../shouldHideEvent';
|
import shouldHideEvent from '../../shouldHideEvent';
|
||||||
|
@ -80,7 +81,6 @@ import { objectHasDiff } from "../../utils/objects";
|
||||||
import SpaceRoomView from "./SpaceRoomView";
|
import SpaceRoomView from "./SpaceRoomView";
|
||||||
import { IOpts } from "../../createRoom";
|
import { IOpts } from "../../createRoom";
|
||||||
import { replaceableComponent } from "../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../utils/replaceableComponent";
|
||||||
import { omit } from 'lodash';
|
|
||||||
import UIStore from "../../stores/UIStore";
|
import UIStore from "../../stores/UIStore";
|
||||||
|
|
||||||
const DEBUG = false;
|
const DEBUG = false;
|
||||||
|
@ -143,7 +143,7 @@ export interface IState {
|
||||||
searchResults?: XOR<{}, {
|
searchResults?: XOR<{}, {
|
||||||
count: number;
|
count: number;
|
||||||
highlights: string[];
|
highlights: string[];
|
||||||
results: MatrixEvent[];
|
results: SearchResult[];
|
||||||
next_batch: string; // eslint-disable-line camelcase
|
next_batch: string; // eslint-disable-line camelcase
|
||||||
}>;
|
}>;
|
||||||
searchHighlights?: string[];
|
searchHighlights?: string[];
|
||||||
|
@ -572,16 +572,12 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
const hasPropsDiff = objectHasDiff(this.props, nextProps);
|
||||||
|
|
||||||
// React only shallow comparison and we only want to trigger
|
const { upgradeRecommendation, ...state } = this.state;
|
||||||
// a component re-render if a room requires an upgrade
|
const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
|
||||||
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
|
|
||||||
|
|
||||||
const state = omit(this.state, ['upgradeRecommendation']);
|
|
||||||
const newState = omit(nextState, ['upgradeRecommendation'])
|
|
||||||
|
|
||||||
const hasStateDiff =
|
const hasStateDiff =
|
||||||
objectHasDiff(state, newState) ||
|
newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
|
||||||
(newUpgradeRecommendation.needsUpgrade === true)
|
objectHasDiff(state, newState);
|
||||||
|
|
||||||
return hasPropsDiff || hasStateDiff;
|
return hasPropsDiff || hasStateDiff;
|
||||||
}
|
}
|
||||||
|
@ -701,16 +697,11 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
room_id: this.state.room.roomId,
|
room_id: this.state.room.roomId,
|
||||||
event_id: this.state.initialEventId,
|
event_id: this.state.initialEventId,
|
||||||
highlighted: false,
|
highlighted: false,
|
||||||
|
replyingToEvent: this.state.replyToEvent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private onLayoutChange = () => {
|
|
||||||
this.setState({
|
|
||||||
layout: SettingsStore.getValue("layout"),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private onRightPanelStoreUpdate = () => {
|
private onRightPanelStoreUpdate = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
|
||||||
|
@ -1644,29 +1635,27 @@ export default class RoomView extends React.Component<IProps, IState> {
|
||||||
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
let auxPanelMaxHeight = UIStore.instance.windowHeight -
|
||||||
(54 + // height of RoomHeader
|
(54 + // height of RoomHeader
|
||||||
36 + // height of the status area
|
36 + // height of the status area
|
||||||
51 + // minimum height of the message compmoser
|
51 + // minimum height of the message composer
|
||||||
120); // amount of desired scrollback
|
120); // amount of desired scrollback
|
||||||
|
|
||||||
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
|
||||||
// but it's better than the video going missing entirely
|
// but it's better than the video going missing entirely
|
||||||
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
|
||||||
|
|
||||||
this.setState({auxPanelMaxHeight: auxPanelMaxHeight});
|
if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
|
||||||
|
this.setState({ auxPanelMaxHeight });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onStatusBarVisible = () => {
|
private onStatusBarVisible = () => {
|
||||||
if (this.unmounted) return;
|
if (this.unmounted || this.state.statusBarVisible) return;
|
||||||
this.setState({
|
this.setState({ statusBarVisible: true });
|
||||||
statusBarVisible: true,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onStatusBarHidden = () => {
|
private onStatusBarHidden = () => {
|
||||||
// This is currently not desired as it is annoying if it keeps expanding and collapsing
|
// This is currently not desired as it is annoying if it keeps expanding and collapsing
|
||||||
if (this.unmounted) return;
|
if (this.unmounted || !this.state.statusBarVisible) return;
|
||||||
this.setState({
|
this.setState({ statusBarVisible: false });
|
||||||
statusBarVisible: false,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -59,7 +59,7 @@ import IconizedContextMenu, {
|
||||||
} 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 dis from "../../dispatcher/dispatcher";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
|
@ -166,7 +166,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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -18,14 +18,14 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import {LayoutPropType} from "../../settings/Layout";
|
import { LayoutPropType } from "../../settings/Layout";
|
||||||
import React, {createRef} from 'react';
|
import React, { createRef } from 'react';
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline";
|
import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
|
||||||
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window";
|
import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
|
||||||
import { _t } from '../../languageHandler';
|
import { _t } from '../../languageHandler';
|
||||||
import {MatrixClientPeg} from "../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
import RoomContext from "../../contexts/RoomContext";
|
import RoomContext from "../../contexts/RoomContext";
|
||||||
import UserActivity from "../../UserActivity";
|
import UserActivity from "../../UserActivity";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
|
@ -35,10 +35,11 @@ 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 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;
|
||||||
|
@ -439,21 +440,42 @@ class TimelinePanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
onAction = payload => {
|
onAction = payload => {
|
||||||
if (payload.action === 'ignore_state_changed') {
|
switch (payload.action) {
|
||||||
this.forceUpdate();
|
case "ignore_state_changed":
|
||||||
}
|
this.forceUpdate();
|
||||||
if (payload.action === "edit_event") {
|
break;
|
||||||
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
|
||||||
this.setState({editState}, () => {
|
case "edit_event": {
|
||||||
if (payload.event && this._messagePanel.current) {
|
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
|
||||||
this._messagePanel.current.scrollToEventIfNeeded(
|
this.setState({editState}, () => {
|
||||||
payload.event.getId(),
|
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;
|
||||||
}
|
}
|
||||||
if (payload.action === "scroll_to_bottom") {
|
|
||||||
this.jumpToLiveTimeline();
|
case "scroll_to_bottom":
|
||||||
|
this.jumpToLiveTimeline();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -33,6 +33,7 @@ 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 { Action } from "../../../dispatcher/actions";
|
||||||
|
|
||||||
export function canCancel(eventStatus) {
|
export function canCancel(eventStatus) {
|
||||||
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
|
||||||
|
@ -200,7 +201,7 @@ export default class MessageContextMenu extends React.Component {
|
||||||
|
|
||||||
onQuoteClick = () => {
|
onQuoteClick = () => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'quote',
|
action: Action.ComposerInsert,
|
||||||
event: this.props.mxEvent,
|
event: this.props.mxEvent,
|
||||||
});
|
});
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -70,7 +70,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,
|
|
||||||
};
|
|
|
@ -17,7 +17,17 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
redact: () => Promise<void>;
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
isRedacting: boolean;
|
||||||
|
redactionErrorCode: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming a redaction.
|
* A dialog for confirming a redaction.
|
||||||
|
@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
* To avoid this, we keep the dialog open as long as /redact is in progress.
|
* To avoid this, we keep the dialog open as long as /redact is in progress.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
|
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
|
||||||
export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
onParentFinished = async (proceed) => {
|
public onParentFinished = async (proceed: boolean): Promise<void> => {
|
||||||
if (proceed) {
|
if (proceed) {
|
||||||
this.setState({isRedacting: true});
|
this.setState({isRedacting: true});
|
||||||
try {
|
try {
|
||||||
|
@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
if (this.state.isRedacting) {
|
if (this.state.isRedacting) {
|
||||||
if (this.state.redactionErrorCode) {
|
if (this.state.redactionErrorCode) {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
|
@ -19,11 +19,15 @@ import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming a redaction.
|
* A dialog for confirming a redaction.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
|
@replaceableComponent("views.dialogs.ConfirmRedactDialog")
|
||||||
export default class ConfirmRedactDialog extends React.Component {
|
export default class ConfirmRedactDialog extends React.Component<IProps> {
|
||||||
render() {
|
render() {
|
||||||
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
|
||||||
return (
|
return (
|
|
@ -15,13 +15,31 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||||
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { GroupMemberType } from '../../../groups';
|
import { GroupMemberType } from '../../../groups';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {mediaFromMxc} from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
||||||
|
member: RoomMember;
|
||||||
|
// group member object. Supply either this or 'member'
|
||||||
|
groupMember: GroupMemberType;
|
||||||
|
// needed if a group member is specified
|
||||||
|
matrixClient?: MatrixClient,
|
||||||
|
action: string; // eg. 'Ban'
|
||||||
|
title: string; // eg. 'Ban this user?'
|
||||||
|
|
||||||
|
// Whether to display a text field for a reason
|
||||||
|
// If true, the second argument to onFinished will
|
||||||
|
// be the string entered.
|
||||||
|
askReason?: boolean;
|
||||||
|
danger?: boolean;
|
||||||
|
onFinished: (success: boolean, reason?: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A dialog for confirming an operation on another user.
|
* A dialog for confirming an operation on another user.
|
||||||
|
@ -32,53 +50,23 @@ import {mediaFromMxc} from "../../../customisations/Media";
|
||||||
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
* Also tweaks the style for 'dangerous' actions (albeit only with colour)
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
@replaceableComponent("views.dialogs.ConfirmUserActionDialog")
|
||||||
export default class ConfirmUserActionDialog extends React.Component {
|
export default class ConfirmUserActionDialog extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
|
||||||
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
|
|
||||||
member: PropTypes.object,
|
|
||||||
// group member object. Supply either this or 'member'
|
|
||||||
groupMember: GroupMemberType,
|
|
||||||
// needed if a group member is specified
|
|
||||||
matrixClient: PropTypes.instanceOf(MatrixClient),
|
|
||||||
action: PropTypes.string.isRequired, // eg. 'Ban'
|
|
||||||
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
|
|
||||||
|
|
||||||
// Whether to display a text field for a reason
|
|
||||||
// If true, the second argument to onFinished will
|
|
||||||
// be the string entered.
|
|
||||||
askReason: PropTypes.bool,
|
|
||||||
danger: PropTypes.bool,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
danger: false,
|
danger: false,
|
||||||
askReason: false,
|
askReason: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
public onOk = (): void => {
|
||||||
super(props);
|
this.props.onFinished(true, this.reasonField.current?.value);
|
||||||
|
|
||||||
this._reasonField = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
onOk = () => {
|
|
||||||
let reason;
|
|
||||||
if (this._reasonField) {
|
|
||||||
reason = this._reasonField.value;
|
|
||||||
}
|
|
||||||
this.props.onFinished(true, reason);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onCancel = () => {
|
public onCancel = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
_collectReasonField = e => {
|
public render() {
|
||||||
this._reasonField = e;
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
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');
|
||||||
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
|
||||||
|
@ -92,7 +80,7 @@ export default class ConfirmUserActionDialog extends React.Component {
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={this.onOk}>
|
<form onSubmit={this.onOk}>
|
||||||
<input className="mx_ConfirmUserActionDialog_reasonField"
|
<input className="mx_ConfirmUserActionDialog_reasonField"
|
||||||
ref={this._collectReasonField}
|
ref={this.reasonField}
|
||||||
placeholder={_t("Reason")}
|
placeholder={_t("Reason")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
|
@ -15,22 +15,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
|
interface IProps {
|
||||||
export default class ConfirmWipeDeviceDialog extends React.Component {
|
onFinished: (success: boolean) => void;
|
||||||
static propTypes = {
|
}
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
_onConfirm = () => {
|
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
|
||||||
|
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
|
||||||
|
private onConfirm = (): void => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDecline = () => {
|
private onDecline = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Clear all data")}
|
primaryButton={_t("Clear all data")}
|
||||||
onPrimaryButtonClick={this._onConfirm}
|
onPrimaryButtonClick={this.onConfirm}
|
||||||
primaryButtonClass="danger"
|
primaryButtonClass="danger"
|
||||||
cancelButton={_t("Cancel")}
|
cancelButton={_t("Cancel")}
|
||||||
onCancel={this._onDecline}
|
onCancel={this.onDecline}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
|
@ -15,44 +15,51 @@ 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 dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
interface IProps {
|
||||||
export default class CreateGroupDialog extends React.Component {
|
onFinished: (success: boolean) => void;
|
||||||
static propTypes = {
|
}
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
state = {
|
interface IState {
|
||||||
|
groupName: string;
|
||||||
|
groupId: string;
|
||||||
|
groupIdError: string;
|
||||||
|
creating: boolean;
|
||||||
|
createError: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.dialogs.CreateGroupDialog")
|
||||||
|
export default class CreateGroupDialog extends React.Component<IProps, IState> {
|
||||||
|
public state = {
|
||||||
groupName: '',
|
groupName: '',
|
||||||
groupId: '',
|
groupId: '',
|
||||||
groupError: null,
|
groupIdError: '',
|
||||||
creating: false,
|
creating: false,
|
||||||
createError: null,
|
createError: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGroupNameChange = e => {
|
private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
groupName: e.target.value,
|
groupName: e.currentTarget.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGroupIdChange = e => {
|
private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
groupId: e.target.value,
|
groupId: e.currentTarget.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onGroupIdBlur = e => {
|
private onGroupIdBlur = (): void => {
|
||||||
this._checkGroupId();
|
this.checkGroupId();
|
||||||
};
|
};
|
||||||
|
|
||||||
_checkGroupId(e) {
|
private checkGroupId() {
|
||||||
let error = null;
|
let error = null;
|
||||||
if (!this.state.groupId) {
|
if (!this.state.groupId) {
|
||||||
error = _t("Community IDs cannot be empty.");
|
error = _t("Community IDs cannot be empty.");
|
||||||
|
@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onFormSubmit = e => {
|
private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (this._checkGroupId()) return;
|
if (this.checkGroupId()) return;
|
||||||
|
|
||||||
const profile = {};
|
const profile: any = {};
|
||||||
if (this.state.groupName !== '') {
|
if (this.state.groupName !== '') {
|
||||||
profile.name = this.state.groupName;
|
profile.name = this.state.groupName;
|
||||||
}
|
}
|
||||||
|
@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component {
|
||||||
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
|
||||||
title={_t('Create Community')}
|
title={_t('Create Community')}
|
||||||
>
|
>
|
||||||
<form onSubmit={this._onFormSubmit}>
|
<form onSubmit={this.onFormSubmit}>
|
||||||
<div className="mx_Dialog_content">
|
<div className="mx_Dialog_content">
|
||||||
<div className="mx_CreateGroupDialog_inputRow">
|
<div className="mx_CreateGroupDialog_inputRow">
|
||||||
<div className="mx_CreateGroupDialog_label">
|
<div className="mx_CreateGroupDialog_label">
|
||||||
|
@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<input id="groupname" className="mx_CreateGroupDialog_input"
|
<input id="groupname" className="mx_CreateGroupDialog_input"
|
||||||
autoFocus={true} size="64"
|
autoFocus={true} size={64}
|
||||||
placeholder={_t('Example')}
|
placeholder={_t('Example')}
|
||||||
onChange={this._onGroupNameChange}
|
onChange={this.onGroupNameChange}
|
||||||
value={this.state.groupName}
|
value={this.state.groupName}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
|
||||||
<span className="mx_CreateGroupDialog_prefix">+</span>
|
<span className="mx_CreateGroupDialog_prefix">+</span>
|
||||||
<input id="groupid"
|
<input id="groupid"
|
||||||
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
|
||||||
size="32"
|
size={32}
|
||||||
placeholder={_t('example')}
|
placeholder={_t('example')}
|
||||||
onChange={this._onGroupIdChange}
|
onChange={this.onGroupIdChange}
|
||||||
onBlur={this._onGroupIdBlur}
|
onBlur={this.onGroupIdBlur}
|
||||||
value={this.state.groupId}
|
value={this.state.groupId}
|
||||||
/>
|
/>
|
||||||
<span className="mx_CreateGroupDialog_suffix">
|
<span className="mx_CreateGroupDialog_suffix">
|
|
@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
export default (props) => {
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (props: IProps) => {
|
||||||
const brand = SdkConfig.get().brand;
|
const brand = SdkConfig.get().brand;
|
||||||
|
|
||||||
const _onLogoutClicked = () => {
|
const _onLogoutClicked = () => {
|
||||||
|
@ -40,7 +44,7 @@ export default (props) => {
|
||||||
onFinished: (doLogout) => {
|
onFinished: (doLogout) => {
|
||||||
if (doLogout) {
|
if (doLogout) {
|
||||||
dis.dispatch({action: 'logout'});
|
dis.dispatch({action: 'logout'});
|
||||||
props.onFinished();
|
props.onFinished(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -16,7 +16,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 Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
|
@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv
|
||||||
import StyledCheckbox from "../elements/StyledCheckbox";
|
import StyledCheckbox from "../elements/StyledCheckbox";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
shouldErase: boolean;
|
||||||
|
errStr: string;
|
||||||
|
authData: any; // for UIA
|
||||||
|
authEnabled: boolean; // see usages for information
|
||||||
|
|
||||||
|
// A few strings that are passed to InteractiveAuth for design or are displayed
|
||||||
|
// next to the InteractiveAuth component.
|
||||||
|
bodyText: string;
|
||||||
|
continueText: string;
|
||||||
|
continueKind: string;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
|
@replaceableComponent("views.dialogs.DeactivateAccountDialog")
|
||||||
export default class DeactivateAccountDialog extends React.Component {
|
export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
|
@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
continueKind: null,
|
continueKind: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
this._initAuth(/* shouldErase= */false);
|
this.initAuth(/* shouldErase= */false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStagePhaseChange = (stage, phase) => {
|
private onStagePhaseChange = (stage: string, phase: string): void => {
|
||||||
const dialogAesthetics = {
|
const dialogAesthetics = {
|
||||||
[SSOAuthEntry.PHASE_PREAUTH]: {
|
[SSOAuthEntry.PHASE_PREAUTH]: {
|
||||||
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
|
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
|
||||||
|
@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
this.setState({bodyText, continueText, continueKind});
|
this.setState({bodyText, continueText, continueKind});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUIAuthFinished = (success, result, extra) => {
|
private onUIAuthFinished = (success: boolean, result: Error) => {
|
||||||
if (success) return; // great! makeRequest() will be called too.
|
if (success) return; // great! makeRequest() will be called too.
|
||||||
|
|
||||||
if (result === ERROR_USER_CANCELLED) {
|
if (result === ERROR_USER_CANCELLED) {
|
||||||
this._onCancel();
|
this.onCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("Error during UI Auth:", {result, extra});
|
console.error("Error during UI Auth:", { result });
|
||||||
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onUIAuthComplete = (auth) => {
|
private onUIAuthComplete = (auth: any): void => {
|
||||||
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
|
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
|
||||||
// Deactivation worked - logout & close this dialog
|
// Deactivation worked - logout & close this dialog
|
||||||
Analytics.trackEvent('Account', 'Deactivate Account');
|
Analytics.trackEvent('Account', 'Deactivate Account');
|
||||||
|
@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
_onEraseFieldChange = (ev) => {
|
private onEraseFieldChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||||
this.setState({
|
this.setState({
|
||||||
shouldErase: ev.target.checked,
|
shouldErase: ev.currentTarget.checked,
|
||||||
|
|
||||||
// Disable the auth form because we're going to have to reinitialize the auth
|
// Disable the auth form because we're going to have to reinitialize the auth
|
||||||
// information. We do this because we can't modify the parameters in the UIA
|
// information. We do this because we can't modify the parameters in the UIA
|
||||||
|
@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
// As mentioned above, set up for auth again to get updated UIA session info
|
// As mentioned above, set up for auth again to get updated UIA session info
|
||||||
this._initAuth(/* shouldErase= */ev.target.checked);
|
this.initAuth(/* shouldErase= */ev.currentTarget.checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onCancel() {
|
private onCancel(): void {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_initAuth(shouldErase) {
|
private initAuth(shouldErase: boolean): void {
|
||||||
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
|
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
|
||||||
// If we got here, oops. The server didn't require any auth.
|
// If we got here, oops. The server didn't require any auth.
|
||||||
// Our application lifecycle will catch the error and do the logout bits.
|
// Our application lifecycle will catch the error and do the logout bits.
|
||||||
|
@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
let error = null;
|
let error = null;
|
||||||
|
@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
<InteractiveAuth
|
<InteractiveAuth
|
||||||
matrixClient={MatrixClientPeg.get()}
|
matrixClient={MatrixClientPeg.get()}
|
||||||
authData={this.state.authData}
|
authData={this.state.authData}
|
||||||
makeRequest={this._onUIAuthComplete}
|
makeRequest={this.onUIAuthComplete}
|
||||||
onAuthFinished={this._onUIAuthFinished}
|
onAuthFinished={this.onUIAuthFinished}
|
||||||
onStagePhaseChange={this._onStagePhaseChange}
|
onStagePhaseChange={this.onStagePhaseChange}
|
||||||
continueText={this.state.continueText}
|
continueText={this.state.continueText}
|
||||||
continueKind={this.state.continueKind}
|
continueKind={this.state.continueKind}
|
||||||
/>
|
/>
|
||||||
|
@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
<p>
|
<p>
|
||||||
<StyledCheckbox
|
<StyledCheckbox
|
||||||
checked={this.state.shouldErase}
|
checked={this.state.shouldErase}
|
||||||
onChange={this._onEraseFieldChange}
|
onChange={this.onEraseFieldChange}
|
||||||
>
|
>
|
||||||
{_t(
|
{_t(
|
||||||
"Please forget all messages I have sent when my account is deactivated " +
|
"Please forget all messages I have sent when my account is deactivated " +
|
||||||
|
@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DeactivateAccountDialog.propTypes = {
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
|
@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAccountDataExplorerState {
|
interface IAccountDataExplorerState {
|
||||||
|
[inputId: string]: boolean | string | any;
|
||||||
isRoomAccountData: boolean;
|
isRoomAccountData: boolean;
|
||||||
event?: MatrixEvent;
|
event?: MatrixEvent;
|
||||||
editing: boolean;
|
editing: boolean;
|
||||||
queryEventType: string;
|
queryEventType: string;
|
||||||
[inputId: string]: boolean | string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {
|
||||||
|
|
|
@ -26,37 +26,37 @@ 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 {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.ErrorDialog")
|
interface IProps {
|
||||||
export default class ErrorDialog extends React.Component {
|
onFinished: (success: boolean) => void;
|
||||||
static propTypes = {
|
title?: string;
|
||||||
title: PropTypes.string,
|
description?: React.ReactNode;
|
||||||
description: PropTypes.oneOfType([
|
button?: string;
|
||||||
PropTypes.element,
|
focus?: boolean;
|
||||||
PropTypes.string,
|
headerImage?: string;
|
||||||
]),
|
}
|
||||||
button: PropTypes.string,
|
|
||||||
focus: PropTypes.bool,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
headerImage: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
static defaultProps = {
|
interface IState {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.dialogs.ErrorDialog")
|
||||||
|
export default class ErrorDialog extends React.Component<IProps, IState> {
|
||||||
|
public static defaultProps = {
|
||||||
focus: true,
|
focus: true,
|
||||||
title: null,
|
title: null,
|
||||||
description: null,
|
description: null,
|
||||||
button: null,
|
button: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
onClick = () => {
|
private onClick = () => {
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
|
@ -162,6 +162,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
|
||||||
});
|
});
|
||||||
mockEvent.sender = {
|
mockEvent.sender = {
|
||||||
name: profileInfo.displayname || userId,
|
name: profileInfo.displayname || userId,
|
||||||
|
rawDisplayName: profileInfo.displayname,
|
||||||
userId,
|
userId,
|
||||||
getAvatarUrl: (..._) => {
|
getAvatarUrl: (..._) => {
|
||||||
return avatarUrlForUser(
|
return avatarUrlForUser(
|
||||||
|
|
|
@ -153,8 +153,8 @@ class ThreepidMember extends Member {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDMUserTileProps {
|
interface IDMUserTileProps {
|
||||||
member: RoomMember;
|
member: Member;
|
||||||
onRemove(member: RoomMember): void;
|
onRemove(member: Member): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
|
@ -168,7 +168,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const avatarSize = 20;
|
const avatarSize = 20;
|
||||||
const avatar = this.props.member.isEmail
|
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||||
? <img
|
? <img
|
||||||
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
|
||||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
|
@ -210,9 +210,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IDMRoomTileProps {
|
interface IDMRoomTileProps {
|
||||||
member: RoomMember;
|
member: Member;
|
||||||
lastActiveTs: number;
|
lastActiveTs: number;
|
||||||
onToggle(member: RoomMember): void;
|
onToggle(member: Member): void;
|
||||||
highlightWord: string;
|
highlightWord: string;
|
||||||
isSelected: boolean;
|
isSelected: boolean;
|
||||||
}
|
}
|
||||||
|
@ -270,7 +270,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
}
|
}
|
||||||
|
|
||||||
const avatarSize = 36;
|
const avatarSize = 36;
|
||||||
const avatar = this.props.member.isEmail
|
const avatar = (this.props.member as ThreepidMember).isEmail
|
||||||
? <img
|
? <img
|
||||||
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
src={require("../../../../res/img/icon-email-pill-avatar.svg")}
|
||||||
width={avatarSize} height={avatarSize} />
|
width={avatarSize} height={avatarSize} />
|
||||||
|
@ -298,7 +298,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
const caption = this.props.member.isEmail
|
const caption = (this.props.member as ThreepidMember).isEmail
|
||||||
? _t("Invite by email")
|
? _t("Invite by email")
|
||||||
: this.highlightName(this.props.member.userId);
|
: this.highlightName(this.props.member.userId);
|
||||||
|
|
||||||
|
@ -334,7 +334,7 @@ interface IInviteDialogProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IInviteDialogState {
|
interface IInviteDialogState {
|
||||||
targets: RoomMember[]; // array of Member objects (see interface above)
|
targets: Member[]; // array of Member objects (see interface above)
|
||||||
filterText: string;
|
filterText: string;
|
||||||
recents: { user: Member, userId: string }[];
|
recents: { user: Member, userId: string }[];
|
||||||
numRecentsShown: number;
|
numRecentsShown: number;
|
||||||
|
|
|
@ -1,149 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 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 React, {PureComponent} from 'react';
|
|
||||||
import * as sdk from '../../../index';
|
|
||||||
import { _t } from '../../../languageHandler';
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
|
||||||
import Markdown from '../../../Markdown';
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A dialog for reporting an event.
|
|
||||||
*/
|
|
||||||
@replaceableComponent("views.dialogs.ReportEventDialog")
|
|
||||||
export default class ReportEventDialog extends PureComponent {
|
|
||||||
static propTypes = {
|
|
||||||
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.state = {
|
|
||||||
reason: "",
|
|
||||||
busy: false,
|
|
||||||
err: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_onReasonChange = ({target: {value: reason}}) => {
|
|
||||||
this.setState({ reason });
|
|
||||||
};
|
|
||||||
|
|
||||||
_onCancel = () => {
|
|
||||||
this.props.onFinished(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
_onSubmit = async () => {
|
|
||||||
if (!this.state.reason || !this.state.reason.trim()) {
|
|
||||||
this.setState({
|
|
||||||
err: _t("Please fill why you're reporting."),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
busy: true,
|
|
||||||
err: null,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
const ev = this.props.mxEvent;
|
|
||||||
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
|
||||||
this.props.onFinished(true);
|
|
||||||
} catch (e) {
|
|
||||||
this.setState({
|
|
||||||
busy: false,
|
|
||||||
err: e.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
|
||||||
const Loader = sdk.getComponent('elements.Spinner');
|
|
||||||
const Field = sdk.getComponent('elements.Field');
|
|
||||||
|
|
||||||
let error = null;
|
|
||||||
if (this.state.err) {
|
|
||||||
error = <div className="error">
|
|
||||||
{this.state.err}
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
let progress = null;
|
|
||||||
if (this.state.busy) {
|
|
||||||
progress = (
|
|
||||||
<div className="progress">
|
|
||||||
<Loader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminMessageMD =
|
|
||||||
SdkConfig.get().reportEvent &&
|
|
||||||
SdkConfig.get().reportEvent.adminMessageMD;
|
|
||||||
let adminMessage;
|
|
||||||
if (adminMessageMD) {
|
|
||||||
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
|
||||||
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BaseDialog
|
|
||||||
className="mx_BugReportDialog"
|
|
||||||
onFinished={this.props.onFinished}
|
|
||||||
title={_t('Report Content to Your Homeserver Administrator')}
|
|
||||||
contentId='mx_ReportEventDialog'
|
|
||||||
>
|
|
||||||
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
|
||||||
<p>
|
|
||||||
{
|
|
||||||
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
|
||||||
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
|
||||||
"administrator will not be able to read the message text or view any files or images.")
|
|
||||||
}
|
|
||||||
</p>
|
|
||||||
{adminMessage}
|
|
||||||
<Field
|
|
||||||
className="mx_ReportEventDialog_reason"
|
|
||||||
element="textarea"
|
|
||||||
label={_t("Reason")}
|
|
||||||
rows={5}
|
|
||||||
onChange={this._onReasonChange}
|
|
||||||
value={this.state.reason}
|
|
||||||
disabled={this.state.busy}
|
|
||||||
/>
|
|
||||||
{progress}
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
<DialogButtons
|
|
||||||
primaryButton={_t("Send report")}
|
|
||||||
onPrimaryButtonClick={this._onSubmit}
|
|
||||||
focus={true}
|
|
||||||
onCancel={this._onCancel}
|
|
||||||
disabled={this.state.busy}
|
|
||||||
/>
|
|
||||||
</BaseDialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
445
src/components/views/dialogs/ReportEventDialog.tsx
Normal file
445
src/components/views/dialogs/ReportEventDialog.tsx
Normal file
|
@ -0,0 +1,445 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 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 React from 'react';
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import { ensureDMExists } from "../../../createRoom";
|
||||||
|
import { IDialogProps } from "./IDialogProps";
|
||||||
|
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
||||||
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
import Markdown from '../../../Markdown';
|
||||||
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
import StyledRadioButton from "../elements/StyledRadioButton";
|
||||||
|
|
||||||
|
interface IProps extends IDialogProps {
|
||||||
|
mxEvent: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
// A free-form text describing the abuse.
|
||||||
|
reason: string;
|
||||||
|
busy: boolean;
|
||||||
|
err?: string;
|
||||||
|
// If we know it, the nature of the abuse, as specified by MSC3215.
|
||||||
|
nature?: EXTENDED_NATURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const MODERATED_BY_STATE_EVENT_TYPE = [
|
||||||
|
"org.matrix.msc3215.room.moderation.moderated_by",
|
||||||
|
/**
|
||||||
|
* Unprefixed state event. Not ready for prime time.
|
||||||
|
*
|
||||||
|
* "m.room.moderation.moderated_by"
|
||||||
|
*/
|
||||||
|
];
|
||||||
|
|
||||||
|
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
|
||||||
|
|
||||||
|
// Standard abuse natures.
|
||||||
|
enum NATURE {
|
||||||
|
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
|
||||||
|
TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
|
||||||
|
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
|
||||||
|
SPAM = "org.matrix.msc3215.abuse.nature.spam",
|
||||||
|
OTHER = "org.matrix.msc3215.abuse.nature.other",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NON_STANDARD_NATURE {
|
||||||
|
// Non-standard abuse nature.
|
||||||
|
// It should never leave the client - we use it to fallback to
|
||||||
|
// server-wide abuse reporting.
|
||||||
|
ADMIN = "non-standard.abuse.nature.admin"
|
||||||
|
}
|
||||||
|
|
||||||
|
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
|
||||||
|
|
||||||
|
type Moderation = {
|
||||||
|
// The id of the moderation room.
|
||||||
|
moderationRoomId: string;
|
||||||
|
// The id of the bot in charge of forwarding abuse reports to the moderation room.
|
||||||
|
moderationBotUserId: string;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* A dialog for reporting an event.
|
||||||
|
*
|
||||||
|
* The actual content of the dialog will depend on two things:
|
||||||
|
*
|
||||||
|
* 1. Is `feature_report_to_moderators` enabled?
|
||||||
|
* 2. Does the room support moderation as per MSC3215, i.e. is there
|
||||||
|
* a well-formed state event `m.room.moderation.moderated_by`
|
||||||
|
* /`org.matrix.msc3215.room.moderation.moderated_by`?
|
||||||
|
*/
|
||||||
|
@replaceableComponent("views.dialogs.ReportEventDialog")
|
||||||
|
export default class ReportEventDialog extends React.Component<IProps, IState> {
|
||||||
|
// If the room supports moderation, the moderation information.
|
||||||
|
private moderation?: Moderation;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
let moderatedByRoomId = null;
|
||||||
|
let moderatedByUserId = null;
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("feature_report_to_moderators")) {
|
||||||
|
// The client supports reporting to moderators.
|
||||||
|
// Does the room support it, too?
|
||||||
|
|
||||||
|
// Extract state events to determine whether we should display
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const room = client.getRoom(props.mxEvent.getRoomId());
|
||||||
|
|
||||||
|
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
|
||||||
|
const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
|
||||||
|
if (!stateEvent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Array.isArray(stateEvent)) {
|
||||||
|
// Internal error.
|
||||||
|
throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
|
||||||
|
"should return at most one state event");
|
||||||
|
}
|
||||||
|
const event = stateEvent.event;
|
||||||
|
if (!("content" in event) || typeof event["content"] != "object") {
|
||||||
|
// The room is improperly configured.
|
||||||
|
// Display this debug message for the sake of moderators.
|
||||||
|
console.debug("Moderation error", "state event", stateEventType,
|
||||||
|
"should have an object field `content`, got", event);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const content = event["content"];
|
||||||
|
if (!("room_id" in content) || typeof content["room_id"] != "string") {
|
||||||
|
// The room is improperly configured.
|
||||||
|
// Display this debug message for the sake of moderators.
|
||||||
|
console.debug("Moderation error", "state event", stateEventType,
|
||||||
|
"should have a string field `content.room_id`, got", event);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!("user_id" in content) || typeof content["user_id"] != "string") {
|
||||||
|
// The room is improperly configured.
|
||||||
|
// Display this debug message for the sake of moderators.
|
||||||
|
console.debug("Moderation error", "state event", stateEventType,
|
||||||
|
"should have a string field `content.user_id`, got", event);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
moderatedByRoomId = content["room_id"];
|
||||||
|
moderatedByUserId = content["user_id"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moderatedByRoomId && moderatedByUserId) {
|
||||||
|
// The room supports moderation.
|
||||||
|
this.moderation = {
|
||||||
|
moderationRoomId: moderatedByRoomId,
|
||||||
|
moderationBotUserId: moderatedByUserId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
// A free-form text describing the abuse.
|
||||||
|
reason: "",
|
||||||
|
busy: false,
|
||||||
|
err: null,
|
||||||
|
// If specified, the nature of the abuse, as specified by MSC3215.
|
||||||
|
nature: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user has written down a freeform description of the abuse.
|
||||||
|
private onReasonChange = ({target: {value: reason}}): void => {
|
||||||
|
this.setState({ reason });
|
||||||
|
};
|
||||||
|
|
||||||
|
// The user has clicked on a nature.
|
||||||
|
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
|
||||||
|
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE});
|
||||||
|
};
|
||||||
|
|
||||||
|
// The user has clicked "cancel".
|
||||||
|
private onCancel = (): void => {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// The user has clicked "submit".
|
||||||
|
private onSubmit = async () => {
|
||||||
|
let reason = this.state.reason || "";
|
||||||
|
reason = reason.trim();
|
||||||
|
if (this.moderation) {
|
||||||
|
// This room supports moderation.
|
||||||
|
// We need a nature.
|
||||||
|
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
|
||||||
|
if (!this.state.nature ||
|
||||||
|
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
|
||||||
|
&& !reason)
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
err: _t("Please fill why you're reporting."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// This room does not support moderation.
|
||||||
|
// We need a `reason`.
|
||||||
|
if (!reason) {
|
||||||
|
this.setState({
|
||||||
|
err: _t("Please fill why you're reporting."),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
busy: true,
|
||||||
|
err: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const ev = this.props.mxEvent;
|
||||||
|
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
|
||||||
|
const nature: NATURE = this.state.nature;
|
||||||
|
|
||||||
|
// Report to moderators through to the dedicated bot,
|
||||||
|
// as configured in the room's state events.
|
||||||
|
const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
|
||||||
|
await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
|
||||||
|
event_id: ev.getId(),
|
||||||
|
room_id: ev.getRoomId(),
|
||||||
|
moderated_by_id: this.moderation.moderationRoomId,
|
||||||
|
nature,
|
||||||
|
reporter: client.getUserId(),
|
||||||
|
comment: this.state.reason.trim(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Report to homeserver admin through the dedicated Matrix API.
|
||||||
|
await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
|
||||||
|
}
|
||||||
|
this.props.onFinished(true);
|
||||||
|
} catch (e) {
|
||||||
|
this.setState({
|
||||||
|
busy: false,
|
||||||
|
err: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
const Loader = sdk.getComponent('elements.Spinner');
|
||||||
|
const Field = sdk.getComponent('elements.Field');
|
||||||
|
|
||||||
|
let error = null;
|
||||||
|
if (this.state.err) {
|
||||||
|
error = <div className="error">
|
||||||
|
{this.state.err}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let progress = null;
|
||||||
|
if (this.state.busy) {
|
||||||
|
progress = (
|
||||||
|
<div className="progress">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminMessageMD =
|
||||||
|
SdkConfig.get().reportEvent &&
|
||||||
|
SdkConfig.get().reportEvent.adminMessageMD;
|
||||||
|
let adminMessage;
|
||||||
|
if (adminMessageMD) {
|
||||||
|
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
|
||||||
|
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.moderation) {
|
||||||
|
// Display report-to-moderator dialog.
|
||||||
|
// We let the user pick a nature.
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
|
||||||
|
let subtitle;
|
||||||
|
switch (this.state.nature) {
|
||||||
|
case NATURE.DISAGREEMENT:
|
||||||
|
subtitle = _t("What this user is writing is wrong.\n" +
|
||||||
|
"This will be reported to the room moderators.");
|
||||||
|
break;
|
||||||
|
case NATURE.TOXIC:
|
||||||
|
subtitle = _t("This user is displaying toxic behaviour, " +
|
||||||
|
"for instance by insulting other users or sharing " +
|
||||||
|
" adult-only content in a family-friendly room " +
|
||||||
|
" or otherwise violating the rules of this room.\n" +
|
||||||
|
"This will be reported to the room moderators.");
|
||||||
|
break;
|
||||||
|
case NATURE.ILLEGAL:
|
||||||
|
subtitle = _t("This user is displaying illegal behaviour, " +
|
||||||
|
"for instance by doxing people or threatening violence.\n" +
|
||||||
|
"This will be reported to the room moderators who may escalate this to legal authorities.");
|
||||||
|
break;
|
||||||
|
case NATURE.SPAM:
|
||||||
|
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
|
||||||
|
"This will be reported to the room moderators.");
|
||||||
|
break;
|
||||||
|
case NON_STANDARD_NATURE.ADMIN:
|
||||||
|
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
|
||||||
|
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||||
|
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||||
|
"This will be reported to the administrators of %(homeserver)s. " +
|
||||||
|
"The administrators will NOT be able to read the encrypted content of this room.",
|
||||||
|
{ homeserver: homeServerName });
|
||||||
|
} else {
|
||||||
|
subtitle = _t("This room is dedicated to illegal or toxic content " +
|
||||||
|
"or the moderators fail to moderate illegal or toxic content.\n" +
|
||||||
|
" This will be reported to the administrators of %(homeserver)s.",
|
||||||
|
{ homeserver: homeServerName });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case NATURE.OTHER:
|
||||||
|
subtitle = _t("Any other reason. Please describe the problem.\n" +
|
||||||
|
"This will be reported to the room moderators.");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
subtitle = _t("Please pick a nature and describe what makes this message abusive.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
className="mx_ReportEventDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t('Report Content')}
|
||||||
|
contentId='mx_ReportEventDialog'
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<StyledRadioButton
|
||||||
|
name = "nature"
|
||||||
|
value = { NATURE.DISAGREEMENT }
|
||||||
|
checked = { this.state.nature == NATURE.DISAGREEMENT }
|
||||||
|
onChange = { this.onNatureChosen }
|
||||||
|
>
|
||||||
|
{_t('Disagree')}
|
||||||
|
</StyledRadioButton>
|
||||||
|
<StyledRadioButton
|
||||||
|
name = "nature"
|
||||||
|
value = { NATURE.TOXIC }
|
||||||
|
checked = { this.state.nature == NATURE.TOXIC }
|
||||||
|
onChange = { this.onNatureChosen }
|
||||||
|
>
|
||||||
|
{_t('Toxic Behaviour')}
|
||||||
|
</StyledRadioButton>
|
||||||
|
<StyledRadioButton
|
||||||
|
name = "nature"
|
||||||
|
value = { NATURE.ILLEGAL }
|
||||||
|
checked = { this.state.nature == NATURE.ILLEGAL }
|
||||||
|
onChange = { this.onNatureChosen }
|
||||||
|
>
|
||||||
|
{_t('Illegal Content')}
|
||||||
|
</StyledRadioButton>
|
||||||
|
<StyledRadioButton
|
||||||
|
name = "nature"
|
||||||
|
value = { NATURE.SPAM }
|
||||||
|
checked = { this.state.nature == NATURE.SPAM }
|
||||||
|
onChange = { this.onNatureChosen }
|
||||||
|
>
|
||||||
|
{_t('Spam or propaganda')}
|
||||||
|
</StyledRadioButton>
|
||||||
|
<StyledRadioButton
|
||||||
|
name = "nature"
|
||||||
|
value = { NON_STANDARD_NATURE.ADMIN }
|
||||||
|
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
|
||||||
|
onChange = { this.onNatureChosen }
|
||||||
|
>
|
||||||
|
{_t('Report the entire room')}
|
||||||
|
</StyledRadioButton>
|
||||||
|
<StyledRadioButton
|
||||||
|
name = "nature"
|
||||||
|
value = { NATURE.OTHER }
|
||||||
|
checked = { this.state.nature == NATURE.OTHER }
|
||||||
|
onChange = { this.onNatureChosen }
|
||||||
|
>
|
||||||
|
{_t('Other')}
|
||||||
|
</StyledRadioButton>
|
||||||
|
<p>
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
<Field
|
||||||
|
className="mx_ReportEventDialog_reason"
|
||||||
|
element="textarea"
|
||||||
|
label={_t("Reason")}
|
||||||
|
rows={5}
|
||||||
|
onChange={this.onReasonChange}
|
||||||
|
value={this.state.reason}
|
||||||
|
disabled={this.state.busy}
|
||||||
|
/>
|
||||||
|
{progress}
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Send report")}
|
||||||
|
onPrimaryButtonClick={this.onSubmit}
|
||||||
|
focus={true}
|
||||||
|
onCancel={this.onCancel}
|
||||||
|
disabled={this.state.busy}
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Report to homeserver admin.
|
||||||
|
// Currently, the API does not support natures.
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
className="mx_ReportEventDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t('Report Content to Your Homeserver Administrator')}
|
||||||
|
contentId='mx_ReportEventDialog'
|
||||||
|
>
|
||||||
|
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
|
||||||
|
<p>
|
||||||
|
{
|
||||||
|
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
|
||||||
|
"your homeserver. If messages in this room are encrypted, your homeserver " +
|
||||||
|
"administrator will not be able to read the message text or view any files " +
|
||||||
|
"or images.")
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
{adminMessage}
|
||||||
|
<Field
|
||||||
|
className="mx_ReportEventDialog_reason"
|
||||||
|
element="textarea"
|
||||||
|
label={_t("Reason")}
|
||||||
|
rows={5}
|
||||||
|
onChange={this.onReasonChange}
|
||||||
|
value={this.state.reason}
|
||||||
|
disabled={this.state.busy}
|
||||||
|
/>
|
||||||
|
{progress}
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Send report")}
|
||||||
|
onPrimaryButtonClick={this.onSubmit}
|
||||||
|
focus={true}
|
||||||
|
onCancel={this.onCancel}
|
||||||
|
disabled={this.state.busy}
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,22 +16,21 @@ limitations under the License.
|
||||||
|
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
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, pickBestLanguage } from '../../../languageHandler';
|
import { _t, pickBestLanguage } from '../../../languageHandler';
|
||||||
|
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
|
import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
|
||||||
|
|
||||||
class TermsCheckbox extends React.PureComponent {
|
interface ITermsCheckboxProps {
|
||||||
static propTypes = {
|
onChange: (url: string, checked: boolean) => void;
|
||||||
onChange: PropTypes.func.isRequired,
|
url: string;
|
||||||
url: PropTypes.string.isRequired,
|
checked: boolean;
|
||||||
checked: PropTypes.bool.isRequired,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
onChange = (ev) => {
|
class TermsCheckbox extends React.PureComponent<ITermsCheckboxProps> {
|
||||||
this.props.onChange(this.props.url, ev.target.checked);
|
private onChange = (ev: React.FormEvent<HTMLInputElement>): void => {
|
||||||
|
this.props.onChange(this.props.url, ev.currentTarget.checked);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ITermsDialogProps {
|
||||||
|
/**
|
||||||
|
* Array of [Service, policies] pairs, where policies is the response from the
|
||||||
|
* /terms endpoint for that service
|
||||||
|
*/
|
||||||
|
policiesAndServicePairs: any[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* urls that the user has already agreed to
|
||||||
|
*/
|
||||||
|
agreedUrls?: string[],
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called with:
|
||||||
|
* * success {bool} True if the user accepted any douments, false if cancelled
|
||||||
|
* * agreedUrls {string[]} List of agreed URLs
|
||||||
|
*/
|
||||||
|
onFinished: (success: boolean, agreedUrls?: string[]) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
agreedUrls: any;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.TermsDialog")
|
@replaceableComponent("views.dialogs.TermsDialog")
|
||||||
export default class TermsDialog extends React.PureComponent {
|
export default class TermsDialog extends React.PureComponent<ITermsDialogProps, IState> {
|
||||||
static propTypes = {
|
|
||||||
/**
|
|
||||||
* Array of [Service, policies] pairs, where policies is the response from the
|
|
||||||
* /terms endpoint for that service
|
|
||||||
*/
|
|
||||||
policiesAndServicePairs: PropTypes.array.isRequired,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* urls that the user has already agreed to
|
|
||||||
*/
|
|
||||||
agreedUrls: PropTypes.arrayOf(PropTypes.string),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called with:
|
|
||||||
* * success {bool} True if the user accepted any douments, false if cancelled
|
|
||||||
* * agreedUrls {string[]} List of agreed URLs
|
|
||||||
*/
|
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
// url -> boolean
|
// url -> boolean
|
||||||
agreedUrls: {},
|
agreedUrls: {},
|
||||||
|
@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancelClick = () => {
|
private onCancelClick = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onNextClick = () => {
|
private onNextClick = (): void => {
|
||||||
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
|
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
|
||||||
}
|
}
|
||||||
|
|
||||||
_nameForServiceType(serviceType, host) {
|
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
|
||||||
switch (serviceType) {
|
switch (serviceType) {
|
||||||
case SERVICE_TYPES.IS:
|
case SERVICE_TYPES.IS:
|
||||||
return <div>{_t("Identity Server")}<br />({host})</div>;
|
return <div>{_t("Identity Server")}<br />({host})</div>;
|
||||||
|
@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_summaryForServiceType(serviceType) {
|
private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element {
|
||||||
switch (serviceType) {
|
switch (serviceType) {
|
||||||
case SERVICE_TYPES.IS:
|
case SERVICE_TYPES.IS:
|
||||||
return <div>
|
return <div>
|
||||||
|
@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onTermsCheckboxChange = (url, checked) => {
|
private onTermsCheckboxChange = (url: string, checked: boolean) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
|
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
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');
|
||||||
|
|
||||||
|
@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
let serviceName;
|
let serviceName;
|
||||||
let summary;
|
let summary;
|
||||||
if (i === 0) {
|
if (i === 0) {
|
||||||
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
|
||||||
summary = this._summaryForServiceType(
|
summary = this.summaryForServiceType(
|
||||||
policiesAndService.service.serviceType,
|
policiesAndService.service.serviceType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
rows.push(<tr key={termDoc[termsLang].url}>
|
rows.push(<tr key={termDoc[termsLang].url}>
|
||||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||||
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
<td>
|
||||||
<span className="mx_TermsDialog_link" />
|
{termDoc[termsLang].name}
|
||||||
</a></td>
|
<a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||||
|
<span className="mx_TermsDialog_link" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
<td><TermsCheckbox
|
<td><TermsCheckbox
|
||||||
url={termDoc[termsLang].url}
|
url={termDoc[termsLang].url}
|
||||||
onChange={this._onTermsCheckboxChange}
|
onChange={this.onTermsCheckboxChange}
|
||||||
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
|
||||||
/></td>
|
/></td>
|
||||||
</tr>);
|
</tr>);
|
||||||
|
@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
fixedWidth={false}
|
fixedWidth={false}
|
||||||
onFinished={this._onCancelClick}
|
onFinished={this.onCancelClick}
|
||||||
title={_t("Terms of Service")}
|
title={_t("Terms of Service")}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
|
@ -197,8 +203,8 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
|
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons primaryButton={_t('Next')}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onCancel={this._onCancelClick}
|
onCancel={this.onCancelClick}
|
||||||
onPrimaryButtonClick={this._onNextClick}
|
onPrimaryButtonClick={this.onNextClick}
|
||||||
primaryDisabled={!enableSubmit}
|
primaryDisabled={!enableSubmit}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
|
@ -16,11 +16,10 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import TabbedView, {Tab} from "../../structures/TabbedView";
|
import TabbedView, {Tab} from "../../structures/TabbedView";
|
||||||
import {_t, _td} from "../../../languageHandler";
|
import {_t, _td} from "../../../languageHandler";
|
||||||
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
|
||||||
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
|
||||||
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
|
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
|
||||||
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
|
||||||
|
@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import {UIFeature} from "../../../settings/UIFeature";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
export const USER_GENERAL_TAB = "USER_GENERAL_TAB";
|
export enum UserTab {
|
||||||
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB";
|
General = "USER_GENERAL_TAB",
|
||||||
export const USER_FLAIR_TAB = "USER_FLAIR_TAB";
|
Appearance = "USER_APPEARANCE_TAB",
|
||||||
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB";
|
Flair = "USER_FLAIR_TAB",
|
||||||
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB";
|
Notifications = "USER_NOTIFICATIONS_TAB",
|
||||||
export const USER_VOICE_TAB = "USER_VOICE_TAB";
|
Preferences = "USER_PREFERENCES_TAB",
|
||||||
export const USER_SECURITY_TAB = "USER_SECURITY_TAB";
|
Voice = "USER_VOICE_TAB",
|
||||||
export const USER_LABS_TAB = "USER_LABS_TAB";
|
Security = "USER_SECURITY_TAB",
|
||||||
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB";
|
Labs = "USER_LABS_TAB",
|
||||||
export const USER_HELP_TAB = "USER_HELP_TAB";
|
Mjolnir = "USER_MJOLNIR_TAB",
|
||||||
|
Help = "USER_HELP_TAB",
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
initialTabId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
mjolnirEnabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.UserSettingsDialog")
|
@replaceableComponent("views.dialogs.UserSettingsDialog")
|
||||||
export default class UserSettingsDialog extends React.Component {
|
export default class UserSettingsDialog extends React.Component<IProps, IState> {
|
||||||
static propTypes = {
|
private mjolnirWatcher: string;
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
initialTabId: PropTypes.string,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
constructor(props) {
|
||||||
super();
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount(): void {
|
public componentDidMount(): void {
|
||||||
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this));
|
this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
public componentWillUnmount(): void {
|
||||||
SettingsStore.unwatchSetting(this._mjolnirWatcher);
|
SettingsStore.unwatchSetting(this.mjolnirWatcher);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mjolnirChanged(settingName, roomId, atLevel, newValue) {
|
private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
|
||||||
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
|
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
|
||||||
this.setState({mjolnirEnabled: newValue});
|
this.setState({mjolnirEnabled: newValue});
|
||||||
}
|
}
|
||||||
|
@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_GENERAL_TAB,
|
UserTab.General,
|
||||||
_td("General"),
|
_td("General"),
|
||||||
"mx_UserSettingsDialog_settingsIcon",
|
"mx_UserSettingsDialog_settingsIcon",
|
||||||
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||||
));
|
));
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_APPEARANCE_TAB,
|
UserTab.Appearance,
|
||||||
_td("Appearance"),
|
_td("Appearance"),
|
||||||
"mx_UserSettingsDialog_appearanceIcon",
|
"mx_UserSettingsDialog_appearanceIcon",
|
||||||
<AppearanceUserSettingsTab />,
|
<AppearanceUserSettingsTab />,
|
||||||
));
|
));
|
||||||
if (SettingsStore.getValue(UIFeature.Flair)) {
|
if (SettingsStore.getValue(UIFeature.Flair)) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_FLAIR_TAB,
|
UserTab.Flair,
|
||||||
_td("Flair"),
|
_td("Flair"),
|
||||||
"mx_UserSettingsDialog_flairIcon",
|
"mx_UserSettingsDialog_flairIcon",
|
||||||
<FlairUserSettingsTab />,
|
<FlairUserSettingsTab />,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_NOTIFICATIONS_TAB,
|
UserTab.Notifications,
|
||||||
_td("Notifications"),
|
_td("Notifications"),
|
||||||
"mx_UserSettingsDialog_bellIcon",
|
"mx_UserSettingsDialog_bellIcon",
|
||||||
<NotificationUserSettingsTab />,
|
<NotificationUserSettingsTab />,
|
||||||
));
|
));
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_PREFERENCES_TAB,
|
UserTab.Preferences,
|
||||||
_td("Preferences"),
|
_td("Preferences"),
|
||||||
"mx_UserSettingsDialog_preferencesIcon",
|
"mx_UserSettingsDialog_preferencesIcon",
|
||||||
<PreferencesUserSettingsTab />,
|
<PreferencesUserSettingsTab />,
|
||||||
|
@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
|
|
||||||
if (SettingsStore.getValue(UIFeature.Voip)) {
|
if (SettingsStore.getValue(UIFeature.Voip)) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_VOICE_TAB,
|
UserTab.Voice,
|
||||||
_td("Voice & Video"),
|
_td("Voice & Video"),
|
||||||
"mx_UserSettingsDialog_voiceIcon",
|
"mx_UserSettingsDialog_voiceIcon",
|
||||||
<VoiceUserSettingsTab />,
|
<VoiceUserSettingsTab />,
|
||||||
|
@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_SECURITY_TAB,
|
UserTab.Security,
|
||||||
_td("Security & Privacy"),
|
_td("Security & Privacy"),
|
||||||
"mx_UserSettingsDialog_securityIcon",
|
"mx_UserSettingsDialog_securityIcon",
|
||||||
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
||||||
|
@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
|
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
|
||||||
) {
|
) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_LABS_TAB,
|
UserTab.Labs,
|
||||||
_td("Labs"),
|
_td("Labs"),
|
||||||
"mx_UserSettingsDialog_labsIcon",
|
"mx_UserSettingsDialog_labsIcon",
|
||||||
<LabsUserSettingsTab />,
|
<LabsUserSettingsTab />,
|
||||||
|
@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component {
|
||||||
}
|
}
|
||||||
if (this.state.mjolnirEnabled) {
|
if (this.state.mjolnirEnabled) {
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_MJOLNIR_TAB,
|
UserTab.Mjolnir,
|
||||||
_td("Ignored users"),
|
_td("Ignored users"),
|
||||||
"mx_UserSettingsDialog_mjolnirIcon",
|
"mx_UserSettingsDialog_mjolnirIcon",
|
||||||
<MjolnirUserSettingsTab />,
|
<MjolnirUserSettingsTab />,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
tabs.push(new Tab(
|
tabs.push(new Tab(
|
||||||
USER_HELP_TAB,
|
UserTab.Help,
|
||||||
_td("Help & About"),
|
_td("Help & About"),
|
||||||
"mx_UserSettingsDialog_helpIcon",
|
"mx_UserSettingsDialog_helpIcon",
|
||||||
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />,
|
<HelpUserSettingsTab closeSettingsFn={() => this.props.onFinished(true)} />,
|
||||||
));
|
));
|
||||||
|
|
||||||
return tabs;
|
return tabs;
|
|
@ -15,22 +15,21 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { _t } from "../../../../languageHandler";
|
||||||
import {_t} from "../../../../languageHandler";
|
|
||||||
import * as sdk from "../../../../index";
|
import * as sdk from "../../../../index";
|
||||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
onFinished: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
|
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
|
||||||
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
export default class ConfirmDestroyCrossSigningDialog extends React.Component<IProps> {
|
||||||
static propTypes = {
|
private onConfirm = (): void => {
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
_onConfirm = () => {
|
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
_onDecline = () => {
|
private onDecline = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Clear cross-signing keys")}
|
primaryButton={_t("Clear cross-signing keys")}
|
||||||
onPrimaryButtonClick={this._onConfirm}
|
onPrimaryButtonClick={this.onConfirm}
|
||||||
primaryButtonClass="danger"
|
primaryButtonClass="danger"
|
||||||
cancelButton={_t("Cancel")}
|
cancelButton={_t("Cancel")}
|
||||||
onCancel={this._onDecline}
|
onCancel={this.onDecline}
|
||||||
/>
|
/>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
|
@ -16,7 +16,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
|
@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons';
|
||||||
import BaseDialog from '../BaseDialog';
|
import BaseDialog from '../BaseDialog';
|
||||||
import Spinner from '../../elements/Spinner';
|
import Spinner from '../../elements/Spinner';
|
||||||
import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
import InteractiveAuthDialog from '../InteractiveAuthDialog';
|
||||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
|
interface IProps {
|
||||||
|
accountPassword?: string;
|
||||||
|
tokenLogin?: boolean;
|
||||||
|
onFinished?: (success: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IState {
|
||||||
|
error: Error | null;
|
||||||
|
canUploadKeysWithPasswordOnly?: boolean;
|
||||||
|
accountPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Walks the user through the process of creating a cross-signing keys. In most
|
* Walks the user through the process of creating a cross-signing keys. In most
|
||||||
|
@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||||
* may need to complete some steps to proceed.
|
* may need to complete some steps to proceed.
|
||||||
*/
|
*/
|
||||||
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
|
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
|
||||||
export default class CreateCrossSigningDialog extends React.PureComponent {
|
export default class CreateCrossSigningDialog extends React.PureComponent<IProps, IState> {
|
||||||
static propTypes = {
|
constructor(props: IProps) {
|
||||||
accountPassword: PropTypes.string,
|
|
||||||
tokenLogin: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
error: null,
|
error: null,
|
||||||
// Does the server offer a UI auth flow with just m.login.password
|
// Does the server offer a UI auth flow with just m.login.password
|
||||||
// for /keys/device_signing/upload?
|
// for /keys/device_signing/upload?
|
||||||
canUploadKeysWithPasswordOnly: null,
|
|
||||||
accountPassword: props.accountPassword || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
if (this.state.accountPassword) {
|
|
||||||
// If we have an account password in memory, let's simplify and
|
// If we have an account password in memory, let's simplify and
|
||||||
// assume it means password auth is also supported for device
|
// assume it means password auth is also supported for device
|
||||||
// signing key upload as well. This avoids hitting the server to
|
// signing key upload as well. This avoids hitting the server to
|
||||||
// test auth flows, which may be slow under high load.
|
// test auth flows, which may be slow under high load.
|
||||||
this.state.canUploadKeysWithPasswordOnly = true;
|
canUploadKeysWithPasswordOnly: props.accountPassword ? true : null,
|
||||||
} else {
|
accountPassword: props.accountPassword || "",
|
||||||
this._queryKeyUploadAuth();
|
};
|
||||||
|
|
||||||
|
if (!this.state.accountPassword) {
|
||||||
|
this.queryKeyUploadAuth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount(): void {
|
||||||
this._bootstrapCrossSigning();
|
this.bootstrapCrossSigning();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _queryKeyUploadAuth() {
|
private async queryKeyUploadAuth(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
|
||||||
// We should never get here: the server should always require
|
// We should never get here: the server should always require
|
||||||
|
@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_doBootstrapUIAuth = async (makeRequest) => {
|
private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
|
||||||
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
|
||||||
await makeRequest({
|
await makeRequest({
|
||||||
type: 'm.login.password',
|
type: 'm.login.password',
|
||||||
|
@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_bootstrapCrossSigning = async () => {
|
private bootstrapCrossSigning = async (): Promise<void> => {
|
||||||
this.setState({
|
this.setState({
|
||||||
error: null,
|
error: null,
|
||||||
});
|
});
|
||||||
|
@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cli.bootstrapCrossSigning({
|
await cli.bootstrapCrossSigning({
|
||||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
|
||||||
});
|
});
|
||||||
this.props.onFinished(true);
|
this.props.onFinished(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (this.props.tokenLogin) {
|
if (this.props.tokenLogin) {
|
||||||
// ignore any failures, we are relying on grace period here
|
// ignore any failures, we are relying on grace period here
|
||||||
this.props.onFinished();
|
this.props.onFinished(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onCancel = () => {
|
private onCancel = (): void => {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
|
||||||
<p>{_t("Unable to set up keys")}</p>
|
<p>{_t("Unable to set up keys")}</p>
|
||||||
<div className="mx_Dialog_buttons">
|
<div className="mx_Dialog_buttons">
|
||||||
<DialogButtons primaryButton={_t('Retry')}
|
<DialogButtons primaryButton={_t('Retry')}
|
||||||
onPrimaryButtonClick={this._bootstrapCrossSigning}
|
onPrimaryButtonClick={this.bootstrapCrossSigning}
|
||||||
onCancel={this._onCancel}
|
onCancel={this.onCancel}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
|
@ -15,47 +15,52 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
|
import SetupEncryptionBody from '../../../structures/auth/SetupEncryptionBody';
|
||||||
import BaseDialog from '../BaseDialog';
|
import BaseDialog from '../BaseDialog';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import { SetupEncryptionStore, PHASE_DONE } from '../../../../stores/SetupEncryptionStore';
|
import { SetupEncryptionStore, Phase } from '../../../../stores/SetupEncryptionStore';
|
||||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||||
|
|
||||||
function iconFromPhase(phase) {
|
function iconFromPhase(phase: Phase) {
|
||||||
if (phase === PHASE_DONE) {
|
if (phase === Phase.Done) {
|
||||||
return require("../../../../../res/img/e2e/verified.svg");
|
return require("../../../../../res/img/e2e/verified.svg");
|
||||||
} else {
|
} else {
|
||||||
return require("../../../../../res/img/e2e/warning.svg");
|
return require("../../../../../res/img/e2e/warning.svg");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
interface IProps {
|
||||||
export default class SetupEncryptionDialog extends React.Component {
|
onFinished: (success: boolean) => void;
|
||||||
static propTypes = {
|
}
|
||||||
onFinished: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor() {
|
interface IState {
|
||||||
super();
|
icon: Phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@replaceableComponent("views.dialogs.security.SetupEncryptionDialog")
|
||||||
|
export default class SetupEncryptionDialog extends React.Component<IProps, IState> {
|
||||||
|
private store: SetupEncryptionStore;
|
||||||
|
|
||||||
|
constructor(props: IProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.store = SetupEncryptionStore.sharedInstance();
|
this.store = SetupEncryptionStore.sharedInstance();
|
||||||
this.state = {icon: iconFromPhase(this.store.phase)};
|
this.state = {icon: iconFromPhase(this.store.phase)};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
public componentDidMount() {
|
||||||
this.store.on("update", this._onStoreUpdate);
|
this.store.on("update", this.onStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
this.store.removeListener("update", this._onStoreUpdate);
|
this.store.removeListener("update", this.onStoreUpdate);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onStoreUpdate = () => {
|
private onStoreUpdate = (): void => {
|
||||||
this.setState({icon: iconFromPhase(this.store.phase)});
|
this.setState({icon: iconFromPhase(this.store.phase)});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
public render() {
|
||||||
return <BaseDialog
|
return <BaseDialog
|
||||||
headerImage={this.state.icon}
|
headerImage={this.state.icon}
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
|
@ -24,7 +24,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
import { formatCommaSeparatedList } from '../../../utils/FormattingUtils';
|
||||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
import { isValid3pidInvite } from "../../../RoomInvite";
|
||||||
import EventListSummary from "./EventListSummary";
|
import EventListSummary from "./EventListSummary";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// An array of member events to summarise
|
// An array of member events to summarise
|
||||||
|
@ -303,7 +303,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getTransitionSequence(events: MatrixEvent[]) {
|
private static getTransitionSequence(events: IUserEvents[]) {
|
||||||
return events.map(MemberEventListSummary.getTransition);
|
return events.map(MemberEventListSummary.getTransition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -315,7 +315,7 @@ export default class MemberEventListSummary extends React.Component<IProps> {
|
||||||
* @returns {string?} the transition type given to this event. This defaults to `null`
|
* @returns {string?} the transition type given to this event. This defaults to `null`
|
||||||
* if a transition is not recognised.
|
* if a transition is not recognised.
|
||||||
*/
|
*/
|
||||||
private static getTransition(e: MatrixEvent): TransitionType {
|
private static getTransition(e: IUserEvents): TransitionType {
|
||||||
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
if (e.mxEvent.getType() === 'm.room.third_party_invite') {
|
||||||
// Handle 3pid invites the same as invites so they get bundled together
|
// Handle 3pid invites the same as invites so they get bundled together
|
||||||
if (!isValid3pidInvite(e.mxEvent)) {
|
if (!isValid3pidInvite(e.mxEvent)) {
|
||||||
|
|
|
@ -304,6 +304,7 @@ export default class ReplyThread extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
async getEvent(eventId) {
|
async getEvent(eventId) {
|
||||||
|
if (!eventId) return null;
|
||||||
const event = this.room.findEventById(eventId);
|
const event = this.room.findEventById(eventId);
|
||||||
if (event) return event;
|
if (event) return event;
|
||||||
|
|
||||||
|
@ -404,6 +405,7 @@ export default class ReplyThread extends React.Component {
|
||||||
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
alwaysShowTimestamps={this.props.alwaysShowTimestamps}
|
||||||
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
enableFlair={SettingsStore.getValue(UIFeature.Flair)}
|
||||||
replacingEventId={ev.replacingEventId()}
|
replacingEventId={ev.replacingEventId()}
|
||||||
|
as="div"
|
||||||
/>
|
/>
|
||||||
</blockquote>;
|
</blockquote>;
|
||||||
});
|
});
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {_t} from "../../../languageHandler";
|
||||||
import {mediaFromContent} from "../../../customisations/Media";
|
import {mediaFromContent} from "../../../customisations/Media";
|
||||||
import {decryptFile} from "../../../utils/DecryptFile";
|
import {decryptFile} from "../../../utils/DecryptFile";
|
||||||
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
import RecordingPlayback from "../voice_messages/RecordingPlayback";
|
||||||
|
import {IMediaEventContent} from "../../../customisations/models/IMediaEventContent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
@ -45,7 +46,7 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
|
||||||
|
|
||||||
public async componentDidMount() {
|
public async componentDidMount() {
|
||||||
let buffer: ArrayBuffer;
|
let buffer: ArrayBuffer;
|
||||||
const content = this.props.mxEvent.getContent();
|
const content: IMediaEventContent = this.props.mxEvent.getContent();
|
||||||
const media = mediaFromContent(content);
|
const media = mediaFromContent(content);
|
||||||
if (media.isEncrypted) {
|
if (media.isEncrypted) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -29,7 +29,9 @@ interface IProps {
|
||||||
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
@replaceableComponent("views.messages.MVoiceOrAudioBody")
|
||||||
export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
export default class MVoiceOrAudioBody extends React.PureComponent<IProps> {
|
||||||
public render() {
|
public render() {
|
||||||
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice'];
|
// MSC2516 is a legacy identifier. See https://github.com/matrix-org/matrix-doc/pull/3245
|
||||||
|
const isVoiceMessage = !!this.props.mxEvent.getContent()['org.matrix.msc2516.voice']
|
||||||
|
|| !!this.props.mxEvent.getContent()['org.matrix.msc3245.voice'];
|
||||||
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
const voiceMessagesEnabled = SettingsStore.getValue("feature_voice_messages");
|
||||||
if (!this.props.forExport && isVoiceMessage && voiceMessagesEnabled) {
|
if (!this.props.forExport && isVoiceMessage && voiceMessagesEnabled) {
|
||||||
return <MVoiceMessageBody {...this.props} />;
|
return <MVoiceMessageBody {...this.props} />;
|
||||||
|
|
|
@ -16,20 +16,19 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import { formatFullDate, formatTime, formatFullTime } from '../../../DateUtils';
|
||||||
import {formatFullDate, formatTime, formatFullTime} from '../../../DateUtils';
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
|
||||||
|
interface IProps {
|
||||||
|
ts: number;
|
||||||
|
showTwelveHour?: boolean;
|
||||||
|
showFullDate?: boolean;
|
||||||
|
showSeconds?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.messages.MessageTimestamp")
|
@replaceableComponent("views.messages.MessageTimestamp")
|
||||||
export default class MessageTimestamp extends React.Component {
|
export default class MessageTimestamp extends React.Component<IProps> {
|
||||||
static propTypes = {
|
public render() {
|
||||||
ts: PropTypes.number.isRequired,
|
|
||||||
showTwelveHour: PropTypes.bool,
|
|
||||||
showFullDate: PropTypes.bool,
|
|
||||||
showSeconds: PropTypes.bool,
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const date = new Date(this.props.ts);
|
const date = new Date(this.props.ts);
|
||||||
let timestamp;
|
let timestamp;
|
||||||
if (this.props.showFullDate) {
|
if (this.props.showFullDate) {
|
||||||
|
@ -41,7 +40,11 @@ export default class MessageTimestamp extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MessageTimestamp" title={formatFullDate(date, this.props.showTwelveHour)} aria-hidden={true}>
|
<span
|
||||||
|
className="mx_MessageTimestamp"
|
||||||
|
title={formatFullDate(date, this.props.showTwelveHour)}
|
||||||
|
aria-hidden={true}
|
||||||
|
>
|
||||||
{timestamp}
|
{timestamp}
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
|
@ -17,10 +17,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Flair from '../elements/Flair.js';
|
import Flair from '../elements/Flair.js';
|
||||||
import FlairStore from '../../../stores/FlairStore';
|
import FlairStore from '../../../stores/FlairStore';
|
||||||
import {getUserNameColorClass} from '../../../utils/FormattingUtils';
|
import { getUserNameColorClass } from '../../../utils/FormattingUtils';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
mxEvent: MatrixEvent;
|
mxEvent: MatrixEvent;
|
||||||
|
|
|
@ -16,12 +16,12 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, {createRef} from 'react';
|
import React, { createRef } from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import highlight from 'highlight.js';
|
import highlight from 'highlight.js';
|
||||||
import * as HtmlUtils from '../../../HtmlUtils';
|
import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
import {formatDate} from '../../../DateUtils';
|
import { formatDate } from '../../../DateUtils';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
|
@ -29,14 +29,16 @@ import { _t } from '../../../languageHandler';
|
||||||
import * as ContextMenu from '../../structures/ContextMenu';
|
import * as ContextMenu from '../../structures/ContextMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
import { pillifyLinks, unmountPills } from '../../../utils/pillify';
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import { IntegrationManagers } from "../../../integrations/IntegrationManagers";
|
||||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
import { isPermalinkHost } from "../../../utils/permalinks/Permalinks";
|
||||||
import {toRightOf} from "../../structures/ContextMenu";
|
import { toRightOf } from "../../structures/ContextMenu";
|
||||||
import {copyPlaintext} from "../../../utils/strings";
|
import { copyPlaintext } from "../../../utils/strings";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
|
||||||
@replaceableComponent("views.messages.TextualBody")
|
@replaceableComponent("views.messages.TextualBody")
|
||||||
export default class TextualBody extends React.Component {
|
export default class TextualBody extends React.Component {
|
||||||
|
@ -390,9 +392,9 @@ export default class TextualBody extends React.Component {
|
||||||
|
|
||||||
onEmoteSenderClick = event => {
|
onEmoteSenderClick = event => {
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
dis.dispatch({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: 'insert_mention',
|
action: Action.ComposerInsert,
|
||||||
user_id: mxEvent.getSender(),
|
userId: mxEvent.getSender(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,8 +17,9 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {_t} from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
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";
|
||||||
|
|
||||||
export const PendingActionSpinner = ({text}) => {
|
export const PendingActionSpinner = ({text}) => {
|
||||||
const Spinner = sdk.getComponent('elements.Spinner');
|
const Spinner = sdk.getComponent('elements.Spinner');
|
||||||
|
@ -31,7 +32,7 @@ export const PendingActionSpinner = ({text}) => {
|
||||||
interface IProps {
|
interface IProps {
|
||||||
waitingForOtherParty: boolean;
|
waitingForOtherParty: boolean;
|
||||||
waitingForNetwork: boolean;
|
waitingForNetwork: boolean;
|
||||||
member: RoomMember;
|
member: RoomMember | User;
|
||||||
onStartVerification: () => Promise<void>;
|
onStartVerification: () => Promise<void>;
|
||||||
isRoomEncrypted: boolean;
|
isRoomEncrypted: boolean;
|
||||||
inDialog: boolean;
|
inDialog: boolean;
|
||||||
|
@ -55,7 +56,7 @@ const EncryptionInfo: React.FC<IProps> = ({
|
||||||
text = _t("Accept on your other login…");
|
text = _t("Accept on your other login…");
|
||||||
} else {
|
} else {
|
||||||
text = _t("Waiting for %(displayName)s to accept…", {
|
text = _t("Waiting for %(displayName)s to accept…", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -38,7 +38,7 @@ import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import RoomViewStore from "../../../stores/RoomViewStore";
|
import RoomViewStore from "../../../stores/RoomViewStore";
|
||||||
import MultiInviter from "../../../utils/MultiInviter";
|
import MultiInviter from "../../../utils/MultiInviter";
|
||||||
import GroupStore from "../../../stores/GroupStore";
|
import GroupStore from "../../../stores/GroupStore";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon from "../rooms/E2EIcon";
|
||||||
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
import { useEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
import { textualPowerLevel } from '../../../Roles';
|
import { textualPowerLevel } from '../../../Roles';
|
||||||
|
@ -48,7 +48,7 @@ import EncryptionPanel from "./EncryptionPanel";
|
||||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||||
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
import { legacyVerifyUser, verifyDevice, verifyUser } from '../../../verification';
|
||||||
import { Action } from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import { USER_SECURITY_TAB } from "../dialogs/UserSettingsDialog";
|
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||||
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
import { useIsEncrypted } from "../../../hooks/useIsEncrypted";
|
||||||
import BaseCard from "./BaseCard";
|
import BaseCard from "./BaseCard";
|
||||||
import { E2EStatus } from "../../../utils/ShieldUtils";
|
import { E2EStatus } from "../../../utils/ShieldUtils";
|
||||||
|
@ -68,6 +68,7 @@ import RoomAvatar from "../avatars/RoomAvatar";
|
||||||
import RoomName from "../elements/RoomName";
|
import RoomName from "../elements/RoomName";
|
||||||
import { mediaFromMxc } from "../../../customisations/Media";
|
import { mediaFromMxc } from "../../../customisations/Media";
|
||||||
import UIStore from "../../../stores/UIStore";
|
import UIStore from "../../../stores/UIStore";
|
||||||
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
|
||||||
export interface IDevice {
|
export interface IDevice {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
|
@ -146,7 +147,7 @@ async function openDMForUser(matrixClient: MatrixClient, userId: string) {
|
||||||
|
|
||||||
type SetUpdating = (updating: boolean) => void;
|
type SetUpdating = (updating: boolean) => void;
|
||||||
|
|
||||||
function useHasCrossSigningKeys(cli: MatrixClient, member: RoomMember, canVerify: boolean, setUpdating: SetUpdating) {
|
function useHasCrossSigningKeys(cli: MatrixClient, member: User, canVerify: boolean, setUpdating: SetUpdating) {
|
||||||
return useAsyncMemo(async () => {
|
return useAsyncMemo(async () => {
|
||||||
if (!canVerify) {
|
if (!canVerify) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
@ -368,9 +369,9 @@ const UserOptionsSection: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
const onInsertPillButton = function() {
|
const onInsertPillButton = function() {
|
||||||
dis.dispatch({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: 'insert_mention',
|
action: Action.ComposerInsert,
|
||||||
user_id: member.userId,
|
userId: member.userId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -971,7 +972,7 @@ interface IRoomPermissions {
|
||||||
canInvite: boolean;
|
canInvite: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPermissions {
|
function useRoomPermissions(cli: MatrixClient, room: Room, user: RoomMember): IRoomPermissions {
|
||||||
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
|
const [roomPermissions, setRoomPermissions] = useState<IRoomPermissions>({
|
||||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||||
modifyLevelMax: -1,
|
modifyLevelMax: -1,
|
||||||
|
@ -1028,7 +1029,7 @@ function useRoomPermissions(cli: MatrixClient, room: Room, user: User): IRoomPer
|
||||||
}
|
}
|
||||||
|
|
||||||
const PowerLevelSection: React.FC<{
|
const PowerLevelSection: React.FC<{
|
||||||
user: User;
|
user: RoomMember;
|
||||||
room: Room;
|
room: Room;
|
||||||
roomPermissions: IRoomPermissions;
|
roomPermissions: IRoomPermissions;
|
||||||
powerLevels: IPowerLevelsContent;
|
powerLevels: IPowerLevelsContent;
|
||||||
|
@ -1037,7 +1038,7 @@ const PowerLevelSection: React.FC<{
|
||||||
return (<PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />);
|
return (<PowerLevelEditor user={user} room={room} roomPermissions={roomPermissions} />);
|
||||||
} else {
|
} else {
|
||||||
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
const powerLevelUsersDefault = powerLevels.users_default || 0;
|
||||||
const powerLevel = parseInt(user.powerLevel, 10);
|
const powerLevel = user.powerLevel;
|
||||||
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
const role = textualPowerLevel(powerLevel, powerLevelUsersDefault);
|
||||||
return (
|
return (
|
||||||
<div className="mx_UserInfo_profileField">
|
<div className="mx_UserInfo_profileField">
|
||||||
|
@ -1048,13 +1049,13 @@ const PowerLevelSection: React.FC<{
|
||||||
};
|
};
|
||||||
|
|
||||||
const PowerLevelEditor: React.FC<{
|
const PowerLevelEditor: React.FC<{
|
||||||
user: User;
|
user: RoomMember;
|
||||||
room: Room;
|
room: Room;
|
||||||
roomPermissions: IRoomPermissions;
|
roomPermissions: IRoomPermissions;
|
||||||
}> = ({user, room, roomPermissions}) => {
|
}> = ({user, room, roomPermissions}) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const [selectedPowerLevel, setSelectedPowerLevel] = useState(parseInt(user.powerLevel, 10));
|
const [selectedPowerLevel, setSelectedPowerLevel] = useState(user.powerLevel);
|
||||||
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
const onPowerChange = useCallback(async (powerLevelStr: string) => {
|
||||||
const powerLevel = parseInt(powerLevelStr, 10);
|
const powerLevel = parseInt(powerLevelStr, 10);
|
||||||
setSelectedPowerLevel(powerLevel);
|
setSelectedPowerLevel(powerLevel);
|
||||||
|
@ -1231,7 +1232,7 @@ const BasicUserInfo: React.FC<{
|
||||||
setPendingUpdateCount(pendingUpdateCount - 1);
|
setPendingUpdateCount(pendingUpdateCount - 1);
|
||||||
}, [pendingUpdateCount]);
|
}, [pendingUpdateCount]);
|
||||||
|
|
||||||
const roomPermissions = useRoomPermissions(cli, room, member);
|
const roomPermissions = useRoomPermissions(cli, room, member as RoomMember);
|
||||||
|
|
||||||
const onSynapseDeactivate = useCallback(async () => {
|
const onSynapseDeactivate = useCallback(async () => {
|
||||||
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
const {finished} = Modal.createTrackedDialog('Synapse User Deactivation', '', QuestionDialog, {
|
||||||
|
@ -1275,12 +1276,26 @@ const BasicUserInfo: React.FC<{
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let memberDetails;
|
||||||
let adminToolsContainer;
|
let adminToolsContainer;
|
||||||
if (room && member.roomId) {
|
if (room && (member as RoomMember).roomId) {
|
||||||
|
// hide the Roles section for DMs as it doesn't make sense there
|
||||||
|
if (!DMRoomMap.shared().getUserIdForRoomId((member as RoomMember).roomId)) {
|
||||||
|
memberDetails = <div className="mx_UserInfo_container">
|
||||||
|
<h3>{ _t("Role") }</h3>
|
||||||
|
<PowerLevelSection
|
||||||
|
powerLevels={powerLevels}
|
||||||
|
user={member as RoomMember}
|
||||||
|
room={room}
|
||||||
|
roomPermissions={roomPermissions}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
adminToolsContainer = (
|
adminToolsContainer = (
|
||||||
<RoomAdminToolsContainer
|
<RoomAdminToolsContainer
|
||||||
powerLevels={powerLevels}
|
powerLevels={powerLevels}
|
||||||
member={member}
|
member={member as RoomMember}
|
||||||
room={room}
|
room={room}
|
||||||
startUpdating={startUpdating}
|
startUpdating={startUpdating}
|
||||||
stopUpdating={stopUpdating}>
|
stopUpdating={stopUpdating}>
|
||||||
|
@ -1309,20 +1324,6 @@ const BasicUserInfo: React.FC<{
|
||||||
spinner = <Spinner />;
|
spinner = <Spinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
let memberDetails;
|
|
||||||
// hide the Roles section for DMs as it doesn't make sense there
|
|
||||||
if (room && member.roomId && !DMRoomMap.shared().getUserIdForRoomId(member.roomId)) {
|
|
||||||
memberDetails = <div className="mx_UserInfo_container">
|
|
||||||
<h3>{ _t("Role") }</h3>
|
|
||||||
<PowerLevelSection
|
|
||||||
powerLevels={powerLevels}
|
|
||||||
user={member}
|
|
||||||
room={room}
|
|
||||||
roomPermissions={roomPermissions}
|
|
||||||
/>
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only display the devices list if our client supports E2E
|
// only display the devices list if our client supports E2E
|
||||||
const cryptoEnabled = cli.isCryptoEnabled();
|
const cryptoEnabled = cli.isCryptoEnabled();
|
||||||
|
|
||||||
|
@ -1349,8 +1350,7 @@ const BasicUserInfo: React.FC<{
|
||||||
const setUpdating = (updating) => {
|
const setUpdating = (updating) => {
|
||||||
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
||||||
};
|
};
|
||||||
const hasCrossSigningKeys =
|
const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify, setUpdating);
|
||||||
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
|
|
||||||
|
|
||||||
const showDeviceListSpinner = devices === undefined;
|
const showDeviceListSpinner = devices === undefined;
|
||||||
if (canVerify) {
|
if (canVerify) {
|
||||||
|
@ -1359,9 +1359,9 @@ const BasicUserInfo: React.FC<{
|
||||||
verifyButton = (
|
verifyButton = (
|
||||||
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {
|
<AccessibleButton className="mx_UserInfo_field mx_UserInfo_verifyButton" onClick={() => {
|
||||||
if (hasCrossSigningKeys) {
|
if (hasCrossSigningKeys) {
|
||||||
verifyUser(member);
|
verifyUser(member as User);
|
||||||
} else {
|
} else {
|
||||||
legacyVerifyUser(member);
|
legacyVerifyUser(member as User);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{_t("Verify")}
|
{_t("Verify")}
|
||||||
|
@ -1381,7 +1381,7 @@ const BasicUserInfo: React.FC<{
|
||||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: USER_SECURITY_TAB,
|
initialTabId: UserTab.Security,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
{ _t("Edit devices") }
|
{ _t("Edit devices") }
|
||||||
|
@ -1409,7 +1409,7 @@ const BasicUserInfo: React.FC<{
|
||||||
<UserOptionsSection
|
<UserOptionsSection
|
||||||
canInvite={roomPermissions.canInvite}
|
canInvite={roomPermissions.canInvite}
|
||||||
isIgnored={isIgnored}
|
isIgnored={isIgnored}
|
||||||
member={member}
|
member={member as RoomMember}
|
||||||
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
isSpace={SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -1428,13 +1428,15 @@ const UserInfoHeader: React.FC<{
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
|
||||||
const onMemberAvatarClick = useCallback(() => {
|
const onMemberAvatarClick = useCallback(() => {
|
||||||
const avatarUrl = member.getMxcAvatarUrl ? member.getMxcAvatarUrl() : member.avatarUrl;
|
const avatarUrl = (member as RoomMember).getMxcAvatarUrl
|
||||||
|
? (member as RoomMember).getMxcAvatarUrl()
|
||||||
|
: (member as User).avatarUrl;
|
||||||
if (!avatarUrl) return;
|
if (!avatarUrl) return;
|
||||||
|
|
||||||
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
const httpUrl = mediaFromMxc(avatarUrl).srcHttp;
|
||||||
const params = {
|
const params = {
|
||||||
src: httpUrl,
|
src: httpUrl,
|
||||||
name: member.name,
|
name: (member as RoomMember).name || (member as User).displayName,
|
||||||
};
|
};
|
||||||
|
|
||||||
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
Modal.createDialog(ImageView, params, "mx_Dialog_lightbox", null, true);
|
||||||
|
@ -1446,13 +1448,13 @@ const UserInfoHeader: React.FC<{
|
||||||
<div>
|
<div>
|
||||||
<MemberAvatar
|
<MemberAvatar
|
||||||
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
key={member.userId} // to instantly blank the avatar when UserInfo changes members
|
||||||
member={member}
|
member={member as RoomMember}
|
||||||
width={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
width={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||||
height={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
height={2 * 0.3 * UIStore.instance.windowHeight} // 2x@30vh
|
||||||
resizeMethod="scale"
|
resizeMethod="scale"
|
||||||
fallbackUserId={member.userId}
|
fallbackUserId={member.userId}
|
||||||
onClick={onMemberAvatarClick}
|
onClick={onMemberAvatarClick}
|
||||||
urls={member.avatarUrl ? [member.avatarUrl] : undefined} />
|
urls={(member as User).avatarUrl ? [(member as User).avatarUrl] : undefined} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1469,7 +1471,11 @@ const UserInfoHeader: React.FC<{
|
||||||
presenceCurrentlyActive = member.user.currentlyActive;
|
presenceCurrentlyActive = member.user.currentlyActive;
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_custom_status")) {
|
if (SettingsStore.getValue("feature_custom_status")) {
|
||||||
statusMessage = member.user._unstable_statusMessage;
|
if ((member as RoomMember).user) {
|
||||||
|
statusMessage = member.user.unstable_statusMessage;
|
||||||
|
} else {
|
||||||
|
statusMessage = (member as unknown as User).unstable_statusMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1500,7 +1506,7 @@ const UserInfoHeader: React.FC<{
|
||||||
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
e2eIcon = <E2EIcon size={18} status={e2eStatus} isUser={true} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayName = member.rawDisplayName || member.displayname;
|
const displayName = (member as RoomMember).rawDisplayName || (member as GroupMember).displayname;
|
||||||
return <React.Fragment>
|
return <React.Fragment>
|
||||||
{ avatarElement }
|
{ avatarElement }
|
||||||
|
|
||||||
|
|
|
@ -22,6 +22,7 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||||
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
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 {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
import {ReciprocateQRCode} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
|
import {SAS} from "matrix-js-sdk/src/crypto/verification/SAS";
|
||||||
|
|
||||||
|
@ -51,7 +52,7 @@ enum VerificationPhase {
|
||||||
interface IProps {
|
interface IProps {
|
||||||
layout: string;
|
layout: string;
|
||||||
request: VerificationRequest;
|
request: VerificationRequest;
|
||||||
member: RoomMember;
|
member: RoomMember | User;
|
||||||
phase: VerificationPhase;
|
phase: VerificationPhase;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isRoomEncrypted: boolean;
|
isRoomEncrypted: boolean;
|
||||||
|
@ -134,7 +135,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
qrBlock = <div className="mx_UserInfo_container">
|
qrBlock = <div className="mx_UserInfo_container">
|
||||||
<h3>{_t("Verify by scanning")}</h3>
|
<h3>{_t("Verify by scanning")}</h3>
|
||||||
<p>{_t("Ask %(displayName)s to scan your code:", {
|
<p>{_t("Ask %(displayName)s to scan your code:", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||||
})}</p>
|
})}</p>
|
||||||
|
|
||||||
<div className="mx_VerificationPanel_qrCode">
|
<div className="mx_VerificationPanel_qrCode">
|
||||||
|
@ -205,7 +206,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
const description = request.isSelfVerification ?
|
const description = request.isSelfVerification ?
|
||||||
_t("Almost there! Is your other session showing the same shield?") :
|
_t("Almost there! Is your other session showing the same shield?") :
|
||||||
_t("Almost there! Is %(displayName)s showing the same shield?", {
|
_t("Almost there! Is %(displayName)s showing the same shield?", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||||
});
|
});
|
||||||
let body: JSX.Element;
|
let body: JSX.Element;
|
||||||
if (this.state.reciprocateQREvent) {
|
if (this.state.reciprocateQREvent) {
|
||||||
|
@ -264,7 +265,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
description = _t("You've successfully verified %(displayName)s!", {
|
description = _t("You've successfully verified %(displayName)s!", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,7 +303,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
text = _t("You cancelled verification on your other session.");
|
text = _t("You cancelled verification on your other session.");
|
||||||
} else {
|
} else {
|
||||||
text = _t("%(displayName)s cancelled verification.", {
|
text = _t("%(displayName)s cancelled verification.", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: (member as User).displayName || (member as RoomMember).name || member.userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
text = `${text} ${startAgainInstruction}`;
|
text = `${text} ${startAgainInstruction}`;
|
||||||
|
@ -325,7 +326,7 @@ export default class VerificationPanel extends React.PureComponent<IProps, IStat
|
||||||
public render() {
|
public render() {
|
||||||
const {member, phase, request} = this.props;
|
const {member, phase, request} = this.props;
|
||||||
|
|
||||||
const displayName = member.displayName || member.name || member.userId;
|
const displayName = (member as User).displayName || (member as RoomMember).name || member.userId;
|
||||||
|
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
case PHASE_READY:
|
case PHASE_READY:
|
||||||
|
|
|
@ -82,13 +82,6 @@ export default class AppsDrawer extends React.Component {
|
||||||
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
this.props.resizeNotifier.off("isResizing", this.onIsResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
|
||||||
// eslint-disable-next-line camelcase
|
|
||||||
UNSAFE_componentWillReceiveProps(newProps) {
|
|
||||||
// Room has changed probably, update apps
|
|
||||||
this._updateApps();
|
|
||||||
}
|
|
||||||
|
|
||||||
onIsResizing = (resizing) => {
|
onIsResizing = (resizing) => {
|
||||||
// This one is the vertical, ie. change height of apps drawer
|
// This one is the vertical, ie. change height of apps drawer
|
||||||
this.setState({ resizingVertical: resizing });
|
this.setState({ resizingVertical: resizing });
|
||||||
|
@ -141,7 +134,10 @@ export default class AppsDrawer extends React.Component {
|
||||||
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
_getAppsHash = (apps) => apps.map(app => app.id).join("~");
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
if (prevProps.userId !== this.props.userId || prevProps.room !== this.props.room) {
|
||||||
|
// Room has changed, update apps
|
||||||
|
this._updateApps();
|
||||||
|
} else if (this._getAppsHash(this.state.apps) !== this._getAppsHash(prevState.apps)) {
|
||||||
this._loadResizerPreferences();
|
this._loadResizerPreferences();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,19 +15,18 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import { Room } from 'matrix-js-sdk/src/models/room'
|
import { Room } from 'matrix-js-sdk/src/models/room'
|
||||||
import dis from "../../../dispatcher/dispatcher";
|
|
||||||
import AppsDrawer from './AppsDrawer';
|
import AppsDrawer from './AppsDrawer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
import { ResizeNotifier } from "../../../utils/ResizeNotifier";
|
||||||
import CallViewForRoom from '../voip/CallViewForRoom';
|
import CallViewForRoom from '../voip/CallViewForRoom';
|
||||||
import {objectHasDiff} from "../../../utils/objects";
|
import { objectHasDiff } from "../../../utils/objects";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// js-sdk room object
|
// js-sdk room object
|
||||||
|
@ -69,19 +68,21 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
counters: this._computeCounters(),
|
counters: this.computeCounters(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("RoomState.events", this._rateLimitedUpdate);
|
if (SettingsStore.getValue("feature_state_counters")) {
|
||||||
|
cli.on("RoomState.events", this.rateLimitedUpdate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
if (cli) {
|
if (cli && SettingsStore.getValue("feature_state_counters")) {
|
||||||
cli.removeListener("RoomState.events", this._rateLimitedUpdate);
|
cli.removeListener("RoomState.events", this.rateLimitedUpdate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,23 +97,11 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onConferenceNotificationClick = (ev, type) => {
|
private rateLimitedUpdate = new RateLimitedFunc(() => {
|
||||||
dis.dispatch({
|
this.setState({ counters: this.computeCounters() });
|
||||||
action: 'place_call',
|
|
||||||
type: type,
|
|
||||||
room_id: this.props.room.roomId,
|
|
||||||
});
|
|
||||||
ev.stopPropagation();
|
|
||||||
ev.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
_rateLimitedUpdate = new RateLimitedFunc(() => {
|
|
||||||
if (SettingsStore.getValue("feature_state_counters")) {
|
|
||||||
this.setState({counters: this._computeCounters()});
|
|
||||||
}
|
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
_computeCounters() {
|
private computeCounters() {
|
||||||
const counters = [];
|
const counters = [];
|
||||||
|
|
||||||
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
|
if (this.props.room && SettingsStore.getValue("feature_state_counters")) {
|
||||||
|
@ -225,7 +214,7 @@ export default class AuxPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AutoHideScrollbar className={classes} style={style} >
|
<AutoHideScrollbar className={classes} style={style}>
|
||||||
{ stateViews }
|
{ stateViews }
|
||||||
{ appsDrawer }
|
{ appsDrawer }
|
||||||
{ callView }
|
{ callView }
|
||||||
|
|
|
@ -16,38 +16,39 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, {createRef, ClipboardEvent} from 'react';
|
import React, { createRef, ClipboardEvent } from 'react';
|
||||||
import {Room} from 'matrix-js-sdk/src/models/room';
|
import { Room } from 'matrix-js-sdk/src/models/room';
|
||||||
|
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
|
||||||
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
|
||||||
|
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
import HistoryManager from '../../../editor/history';
|
import HistoryManager from '../../../editor/history';
|
||||||
import {Caret, setSelection} from '../../../editor/caret';
|
import { Caret, setSelection } from '../../../editor/caret';
|
||||||
import {
|
import {
|
||||||
formatRangeAsQuote,
|
formatRangeAsQuote,
|
||||||
formatRangeAsCode,
|
formatRangeAsCode,
|
||||||
toggleInlineFormat,
|
toggleInlineFormat,
|
||||||
replaceRangeAndMoveCaret,
|
replaceRangeAndMoveCaret,
|
||||||
} from '../../../editor/operations';
|
} from '../../../editor/operations';
|
||||||
import {getCaretOffsetAndText, getRangeForSelection} from '../../../editor/dom';
|
import { getCaretOffsetAndText, getRangeForSelection } from '../../../editor/dom';
|
||||||
import Autocomplete, {generateCompletionDomId} from '../rooms/Autocomplete';
|
import Autocomplete, { generateCompletionDomId } from '../rooms/Autocomplete';
|
||||||
import {getAutoCompleteCreator} from '../../../editor/parts';
|
import { getAutoCompleteCreator } from '../../../editor/parts';
|
||||||
import {parsePlainTextMessage} from '../../../editor/deserialize';
|
import { parseEvent, parsePlainTextMessage } from '../../../editor/deserialize';
|
||||||
import {renderModel} from '../../../editor/render';
|
import { renderModel } from '../../../editor/render';
|
||||||
import TypingStore from "../../../stores/TypingStore";
|
import TypingStore from "../../../stores/TypingStore";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {Key} from "../../../Keyboard";
|
import { Key } from "../../../Keyboard";
|
||||||
import {EMOTICON_TO_EMOJI} from "../../../emoji";
|
import { EMOTICON_TO_EMOJI } from "../../../emoji";
|
||||||
import {CommandCategories, CommandMap, parseCommandString} from "../../../SlashCommands";
|
import { CommandCategories, CommandMap, parseCommandString } from "../../../SlashCommands";
|
||||||
import Range from "../../../editor/range";
|
import Range from "../../../editor/range";
|
||||||
import MessageComposerFormatBar from "./MessageComposerFormatBar";
|
import MessageComposerFormatBar from "./MessageComposerFormatBar";
|
||||||
import DocumentOffset from "../../../editor/offset";
|
import DocumentOffset from "../../../editor/offset";
|
||||||
import {IDiff} from "../../../editor/diff";
|
import { IDiff } from "../../../editor/diff";
|
||||||
import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
import AutocompleteWrapperModel from "../../../editor/autocomplete";
|
||||||
import DocumentPosition from "../../../editor/position";
|
import DocumentPosition from "../../../editor/position";
|
||||||
import {ICompletion} from "../../../autocomplete/Autocompleter";
|
import { ICompletion } from "../../../autocomplete/Autocompleter";
|
||||||
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
import { AutocompleteAction, getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
// matches emoticons which follow the start of a line or whitespace
|
// matches emoticons which follow the start of a line or whitespace
|
||||||
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
const REGEX_EMOTICON_WHITESPACE = new RegExp('(?:^|\\s)(' + EMOTICON_REGEX.source + ')\\s$');
|
||||||
|
@ -716,4 +717,48 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
focus() {
|
focus() {
|
||||||
this.editorRef.current.focus();
|
this.editorRef.current.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public insertMention(userId: string) {
|
||||||
|
const {model} = this.props;
|
||||||
|
const {partCreator} = model;
|
||||||
|
const member = this.props.room.getMember(userId);
|
||||||
|
const displayName = member ?
|
||||||
|
member.rawDisplayName : userId;
|
||||||
|
const caret = this.getCaret();
|
||||||
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
// Insert suffix only if the caret is at the start of the composer
|
||||||
|
const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId);
|
||||||
|
model.transform(() => {
|
||||||
|
const addedLen = model.insert(parts, position);
|
||||||
|
return model.positionForOffset(caret.offset + addedLen, true);
|
||||||
|
});
|
||||||
|
// refocus on composer, as we just clicked "Mention"
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public insertQuotedMessage(event: MatrixEvent) {
|
||||||
|
const {model} = this.props;
|
||||||
|
const {partCreator} = model;
|
||||||
|
const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
|
||||||
|
// add two newlines
|
||||||
|
quoteParts.push(partCreator.newline());
|
||||||
|
quoteParts.push(partCreator.newline());
|
||||||
|
model.transform(() => {
|
||||||
|
const addedLen = model.insert(quoteParts, model.positionForOffset(0));
|
||||||
|
return model.positionForOffset(addedLen, true);
|
||||||
|
});
|
||||||
|
// refocus on composer, as we just clicked "Quote"
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public insertPlaintext(text: string) {
|
||||||
|
const {model} = this.props;
|
||||||
|
const {partCreator} = model;
|
||||||
|
const caret = this.getCaret();
|
||||||
|
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
||||||
|
model.transform(() => {
|
||||||
|
const addedLen = model.insert([partCreator.plain(text)], position);
|
||||||
|
return model.positionForOffset(caret.offset + addedLen, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,25 +16,25 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {_t, _td} from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import EditorModel from '../../../editor/model';
|
import EditorModel from '../../../editor/model';
|
||||||
import {getCaretOffsetAndText} from '../../../editor/dom';
|
import { getCaretOffsetAndText } from '../../../editor/dom';
|
||||||
import {htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand} from '../../../editor/serialize';
|
import { htmlSerializeIfNeeded, textSerialize, containsEmote, stripEmoteCommand } from '../../../editor/serialize';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import { parseEvent } from '../../../editor/deserialize';
|
||||||
import {CommandPartCreator} from '../../../editor/parts';
|
import { CommandPartCreator } from '../../../editor/parts';
|
||||||
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
import EditorStateTransfer from '../../../utils/EditorStateTransfer';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import {EventStatus} from 'matrix-js-sdk/src/models/event';
|
import { EventStatus } from 'matrix-js-sdk/src/models/event';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
import { CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import SendHistoryManager from '../../../SendHistoryManager';
|
import SendHistoryManager from '../../../SendHistoryManager';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ export default class EditMessageComposer extends React.Component {
|
||||||
};
|
};
|
||||||
this._createEditorModel();
|
this._createEditorModel();
|
||||||
window.addEventListener("beforeunload", this._saveStoredEditorState);
|
window.addEventListener("beforeunload", this._saveStoredEditorState);
|
||||||
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setEditorRef = ref => {
|
_setEditorRef = ref => {
|
||||||
|
@ -399,6 +400,7 @@ export default class EditMessageComposer extends React.Component {
|
||||||
if (this._shouldSaveStoredEditorState) {
|
if (this._shouldSaveStoredEditorState) {
|
||||||
this._saveStoredEditorState();
|
this._saveStoredEditorState();
|
||||||
}
|
}
|
||||||
|
dis.unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_createEditorModel() {
|
_createEditorModel() {
|
||||||
|
@ -443,6 +445,18 @@ export default class EditMessageComposer extends React.Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onAction = payload => {
|
||||||
|
if (payload.action === "edit_composer_insert" && this._editorRef) {
|
||||||
|
if (payload.userId) {
|
||||||
|
this._editorRef.insertMention(payload.userId);
|
||||||
|
} else if (payload.event) {
|
||||||
|
this._editorRef.insertQuotedMessage(payload.event);
|
||||||
|
} else if (payload.text) {
|
||||||
|
this._editorRef.insertPlaintext(payload.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this._onKeyDown}>
|
return (<div className={classNames("mx_EditMessageComposer", this.props.className)} onKeyDown={this._onKeyDown}>
|
||||||
|
|
|
@ -46,6 +46,8 @@ import { EditorStateTransfer } from "../../../utils/EditorStateTransfer";
|
||||||
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
import { RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
|
||||||
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState";
|
||||||
import NotificationBadge from "./NotificationBadge";
|
import NotificationBadge from "./NotificationBadge";
|
||||||
|
import {ComposerInsertPayload} from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { Action } from '../../../dispatcher/actions';
|
||||||
|
|
||||||
const eventTileTypes = {
|
const eventTileTypes = {
|
||||||
[EventType.RoomMessage]: 'messages.MessageEvent',
|
[EventType.RoomMessage]: 'messages.MessageEvent',
|
||||||
|
@ -379,7 +381,7 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
EventType.RoomMessage,
|
EventType.RoomMessage,
|
||||||
EventType.RoomMessageEncrypted,
|
EventType.RoomMessageEncrypted,
|
||||||
];
|
];
|
||||||
if (!simpleSendableEvents.includes(this.props.mxEvent.getType())) return false;
|
if (!simpleSendableEvents.includes(this.props.mxEvent.getType() as EventType)) return false;
|
||||||
|
|
||||||
// Default case
|
// Default case
|
||||||
return true;
|
return true;
|
||||||
|
@ -731,9 +733,9 @@ export default class EventTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
onSenderProfileClick = event => {
|
onSenderProfileClick = event => {
|
||||||
const mxEvent = this.props.mxEvent;
|
const mxEvent = this.props.mxEvent;
|
||||||
dis.dispatch({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: 'insert_mention',
|
action: Action.ComposerInsert,
|
||||||
user_id: mxEvent.getSender(),
|
userId: mxEvent.getSender(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -16,11 +16,11 @@ limitations under the License.
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import { MatrixClientPeg } from '../../../MatrixClientPeg';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import {Room} from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { ActionPayload } from "../../../dispatcher/payloads";
|
import { ActionPayload } from "../../../dispatcher/payloads";
|
||||||
import Stickerpicker from './Stickerpicker';
|
import Stickerpicker from './Stickerpicker';
|
||||||
|
@ -28,19 +28,21 @@ import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalin
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import E2EIcon from './E2EIcon';
|
import E2EIcon from './E2EIcon';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import {aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu} from "../../structures/ContextMenu";
|
import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from "../../structures/ContextMenu";
|
||||||
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import {UIFeature} from "../../../settings/UIFeature";
|
import { UIFeature } from "../../../settings/UIFeature";
|
||||||
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
|
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
|
||||||
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
|
import { VoiceRecordingStore } from "../../../stores/VoiceRecordingStore";
|
||||||
import {RecordingState} from "../../../voice/VoiceRecording";
|
import { RecordingState } from "../../../voice/VoiceRecording";
|
||||||
import Tooltip, {Alignment} from "../elements/Tooltip";
|
import Tooltip, { Alignment } from "../elements/Tooltip";
|
||||||
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../../utils/ResizeNotifier";
|
||||||
import { E2EStatus } from '../../../utils/ShieldUtils';
|
import { E2EStatus } from '../../../utils/ShieldUtils';
|
||||||
import SendMessageComposer from "./SendMessageComposer";
|
import SendMessageComposer from "./SendMessageComposer";
|
||||||
|
import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload";
|
||||||
|
import { Action } from "../../../dispatcher/actions";
|
||||||
|
|
||||||
interface IComposerAvatarProps {
|
interface IComposerAvatarProps {
|
||||||
me: object;
|
me: object;
|
||||||
|
@ -316,10 +318,10 @@ export default class MessageComposer extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
addEmoji(emoji) {
|
addEmoji(emoji: string) {
|
||||||
dis.dispatch({
|
dis.dispatch<ComposerInsertPayload>({
|
||||||
action: "insert_emoji",
|
action: Action.ComposerInsert,
|
||||||
emoji,
|
text: emoji,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
|
||||||
@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
|
@replaceableComponent("views.rooms.RoomUpgradeWarningBar")
|
||||||
export default class RoomUpgradeWarningBar extends React.Component {
|
export default class RoomUpgradeWarningBar extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
room: PropTypes.object.isRequired,
|
room: PropTypes.object.isRequired,
|
||||||
recommendation: PropTypes.object.isRequired,
|
recommendation: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -27,27 +27,26 @@ import {
|
||||||
startsWith,
|
startsWith,
|
||||||
stripPrefix,
|
stripPrefix,
|
||||||
} from '../../../editor/serialize';
|
} from '../../../editor/serialize';
|
||||||
import {CommandPartCreator} from '../../../editor/parts';
|
import { CommandPartCreator } from '../../../editor/parts';
|
||||||
import BasicMessageComposer from "./BasicMessageComposer";
|
import BasicMessageComposer from "./BasicMessageComposer";
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {parseEvent} from '../../../editor/deserialize';
|
import { findEditableEvent } from '../../../utils/EventUtils';
|
||||||
import {findEditableEvent} from '../../../utils/EventUtils';
|
|
||||||
import SendHistoryManager from "../../../SendHistoryManager";
|
import SendHistoryManager from "../../../SendHistoryManager";
|
||||||
import {CommandCategories, getCommand} from '../../../SlashCommands';
|
import { CommandCategories, getCommand } from '../../../SlashCommands';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import {_t, _td} from '../../../languageHandler';
|
import { _t, _td } from '../../../languageHandler';
|
||||||
import ContentMessages from '../../../ContentMessages';
|
import ContentMessages from '../../../ContentMessages';
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
import {Action} from "../../../dispatcher/actions";
|
import { Action } from "../../../dispatcher/actions";
|
||||||
import {containsEmoji} from "../../../effects/utils";
|
import { containsEmoji } from "../../../effects/utils";
|
||||||
import {CHAT_EFFECTS} from '../../../effects';
|
import { CHAT_EFFECTS } from '../../../effects';
|
||||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../MatrixClientPeg";
|
||||||
import EMOJI_REGEX from 'emojibase-regex';
|
import EMOJI_REGEX from 'emojibase-regex';
|
||||||
import {getKeyBindingsManager, MessageComposerAction} from '../../../KeyBindingsManager';
|
import { getKeyBindingsManager, MessageComposerAction } from '../../../KeyBindingsManager';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import { replaceableComponent } from "../../../utils/replaceableComponent";
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
|
|
||||||
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
|
||||||
|
@ -486,62 +485,18 @@ export default class SendMessageComposer extends React.Component {
|
||||||
case Action.FocusComposer:
|
case Action.FocusComposer:
|
||||||
this._editorRef && this._editorRef.focus();
|
this._editorRef && this._editorRef.focus();
|
||||||
break;
|
break;
|
||||||
case 'insert_mention':
|
case "send_composer_insert":
|
||||||
this._insertMention(payload.user_id);
|
if (payload.userId) {
|
||||||
break;
|
this._editorRef && this._editorRef.insertMention(payload.userId);
|
||||||
case 'quote':
|
} else if (payload.event) {
|
||||||
this._insertQuotedMessage(payload.event);
|
this._editorRef && this._editorRef.insertQuotedMessage(payload.event);
|
||||||
break;
|
} else if (payload.text) {
|
||||||
case 'insert_emoji':
|
this._editorRef && this._editorRef.insertPlaintext(payload.text);
|
||||||
this._insertEmoji(payload.emoji);
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
_insertMention(userId) {
|
|
||||||
const {model} = this;
|
|
||||||
const {partCreator} = model;
|
|
||||||
const member = this.props.room.getMember(userId);
|
|
||||||
const displayName = member ?
|
|
||||||
member.rawDisplayName : userId;
|
|
||||||
const caret = this._editorRef.getCaret();
|
|
||||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
|
||||||
// Insert suffix only if the caret is at the start of the composer
|
|
||||||
const parts = partCreator.createMentionParts(caret.offset === 0, displayName, userId);
|
|
||||||
model.transform(() => {
|
|
||||||
const addedLen = model.insert(parts, position);
|
|
||||||
return model.positionForOffset(caret.offset + addedLen, true);
|
|
||||||
});
|
|
||||||
// refocus on composer, as we just clicked "Mention"
|
|
||||||
this._editorRef && this._editorRef.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
_insertQuotedMessage(event) {
|
|
||||||
const {model} = this;
|
|
||||||
const {partCreator} = model;
|
|
||||||
const quoteParts = parseEvent(event, partCreator, {isQuotedMessage: true});
|
|
||||||
// add two newlines
|
|
||||||
quoteParts.push(partCreator.newline());
|
|
||||||
quoteParts.push(partCreator.newline());
|
|
||||||
model.transform(() => {
|
|
||||||
const addedLen = model.insert(quoteParts, model.positionForOffset(0));
|
|
||||||
return model.positionForOffset(addedLen, true);
|
|
||||||
});
|
|
||||||
// refocus on composer, as we just clicked "Quote"
|
|
||||||
this._editorRef && this._editorRef.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
_insertEmoji = (emoji) => {
|
|
||||||
const {model} = this;
|
|
||||||
const {partCreator} = model;
|
|
||||||
const caret = this._editorRef.getCaret();
|
|
||||||
const position = model.positionForOffset(caret.offset, caret.atNodeEnd);
|
|
||||||
model.transform(() => {
|
|
||||||
const addedLen = model.insert([partCreator.plain(emoji)], position);
|
|
||||||
return model.positionForOffset(caret.offset + addedLen, true);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
_onPaste = (event) => {
|
_onPaste = (event) => {
|
||||||
const {clipboardData} = event;
|
const {clipboardData} = event;
|
||||||
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
|
// Prioritize text on the clipboard over files as Office on macOS puts a bitmap
|
||||||
|
|
|
@ -77,7 +77,8 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
size: this.state.recorder.contentLength,
|
size: this.state.recorder.contentLength,
|
||||||
},
|
},
|
||||||
|
|
||||||
// MSC1767 experiment
|
// MSC1767 + Ideals of MSC2516 as MSC3245
|
||||||
|
// https://github.com/matrix-org/matrix-doc/pull/3245
|
||||||
"org.matrix.msc1767.text": "Voice message",
|
"org.matrix.msc1767.text": "Voice message",
|
||||||
"org.matrix.msc1767.file": {
|
"org.matrix.msc1767.file": {
|
||||||
url: mxc,
|
url: mxc,
|
||||||
|
@ -88,14 +89,10 @@ export default class VoiceRecordComposerTile extends React.PureComponent<IProps,
|
||||||
"org.matrix.msc1767.audio": {
|
"org.matrix.msc1767.audio": {
|
||||||
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
duration: Math.round(this.state.recorder.durationSeconds * 1000),
|
||||||
|
|
||||||
// Events can't have floats, so we try to maintain resolution by using 1024
|
// https://github.com/matrix-org/matrix-doc/pull/3246
|
||||||
// as a maximum value. The waveform contains values between zero and 1, so this
|
|
||||||
// should come out largely sane.
|
|
||||||
//
|
|
||||||
// We're expecting about one data point per second of audio.
|
|
||||||
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
|
waveform: this.state.recorder.getPlayback().waveform.map(v => Math.round(v * 1024)),
|
||||||
},
|
},
|
||||||
"org.matrix.msc2516.voice": {}, // No content, this is a rendering hint
|
"org.matrix.msc3245.voice": {}, // No content, this is a rendering hint
|
||||||
});
|
});
|
||||||
await this.disposeRecording();
|
await this.disposeRecording();
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,14 +44,11 @@ export default class BridgeSettingsTab extends React.Component<IProps> {
|
||||||
return <BridgeTile key={event.getId()} room={room} ev={event} />;
|
return <BridgeTile key={event.getId()} room={room} ev={event} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
static getBridgeStateEvents(roomId: string) {
|
static getBridgeStateEvents(roomId: string): MatrixEvent[] {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const roomState = client.getRoom(roomId).currentState;
|
const roomState = client.getRoom(roomId).currentState;
|
||||||
|
|
||||||
return BRIDGE_EVENT_TYPES.map(typeName => {
|
return BRIDGE_EVENT_TYPES.map(typeName => roomState.getStateEvents(typeName)).flat(1);
|
||||||
const events = roomState.events.get(typeName);
|
|
||||||
return events ? Array.from(events.values()) : [];
|
|
||||||
}).flat(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -32,7 +32,7 @@ import * as ContextMenu from "../../../../structures/ContextMenu";
|
||||||
import { toRightOf } from "../../../../structures/ContextMenu";
|
import { toRightOf } from "../../../../structures/ContextMenu";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
closeSettingsFn: () => {};
|
closeSettingsFn: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
|
|
|
@ -29,7 +29,7 @@ import AccessibleButton from "../elements/AccessibleButton";
|
||||||
import {BetaPill} from "../beta/BetaCard";
|
import {BetaPill} from "../beta/BetaCard";
|
||||||
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 "../dialogs/UserSettingsDialog";
|
import { UserTab } from "../dialogs/UserSettingsDialog";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import withValidation from "../elements/Validation";
|
import withValidation from "../elements/Validation";
|
||||||
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
|
||||||
|
@ -224,7 +224,7 @@ const SpaceCreateMenu = ({ onFinished }) => {
|
||||||
onFinished();
|
onFinished();
|
||||||
defaultDispatcher.dispatch({
|
defaultDispatcher.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: USER_LABS_TAB,
|
initialTabId: UserTab.Labs,
|
||||||
});
|
});
|
||||||
}} />
|
}} />
|
||||||
{ body }
|
{ body }
|
||||||
|
|
|
@ -21,17 +21,20 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import dis from '../../../dispatcher/dispatcher';
|
import dis from '../../../dispatcher/dispatcher';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import { ActionPayload } from '../../../dispatcher/payloads';
|
import { ActionPayload } from '../../../dispatcher/payloads';
|
||||||
import CallHandler from '../../../CallHandler';
|
import CallHandler, { AudioID } from '../../../CallHandler';
|
||||||
import RoomAvatar from '../avatars/RoomAvatar';
|
import RoomAvatar from '../avatars/RoomAvatar';
|
||||||
import FormButton from '../elements/FormButton';
|
import FormButton from '../elements/FormButton';
|
||||||
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
import { CallState } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import AccessibleTooltipButton from '../elements/AccessibleTooltipButton';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
incomingCall: any;
|
incomingCall: any;
|
||||||
|
silenced: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@replaceableComponent("views.voip.IncomingCallBox")
|
@replaceableComponent("views.voip.IncomingCallBox")
|
||||||
|
@ -44,6 +47,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
this.state = {
|
this.state = {
|
||||||
incomingCall: null,
|
incomingCall: null,
|
||||||
|
silenced: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,6 +62,7 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
if (call && call.state === CallState.Ringing) {
|
if (call && call.state === CallState.Ringing) {
|
||||||
this.setState({
|
this.setState({
|
||||||
incomingCall: call,
|
incomingCall: call,
|
||||||
|
silenced: false, // Reset silenced state for new call
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -84,6 +89,13 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onSilenceClick: React.MouseEventHandler = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const newState = !this.state.silenced
|
||||||
|
this.setState({silenced: newState});
|
||||||
|
newState ? CallHandler.sharedInstance().pause(AudioID.Ring) : CallHandler.sharedInstance().play(AudioID.Ring);
|
||||||
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
if (!this.state.incomingCall) {
|
if (!this.state.incomingCall) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -107,6 +119,12 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const silenceClass = classNames({
|
||||||
|
"mx_IncomingCallBox_iconButton": true,
|
||||||
|
"mx_IncomingCallBox_unSilence": this.state.silenced,
|
||||||
|
"mx_IncomingCallBox_silence": !this.state.silenced,
|
||||||
|
});
|
||||||
|
|
||||||
return <div className="mx_IncomingCallBox">
|
return <div className="mx_IncomingCallBox">
|
||||||
<div className="mx_IncomingCallBox_CallerInfo">
|
<div className="mx_IncomingCallBox_CallerInfo">
|
||||||
<RoomAvatar
|
<RoomAvatar
|
||||||
|
@ -118,6 +136,11 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
|
||||||
<h1>{caller}</h1>
|
<h1>{caller}</h1>
|
||||||
<p>{incomingCallText}</p>
|
<p>{incomingCallText}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<AccessibleTooltipButton
|
||||||
|
className={silenceClass}
|
||||||
|
onClick={this.onSilenceClick}
|
||||||
|
title={this.state.silenced ? _t("Sound on"): _t("Silence call")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_IncomingCallBox_buttons">
|
<div className="mx_IncomingCallBox_buttons">
|
||||||
<FormButton
|
<FormButton
|
||||||
|
|
|
@ -159,4 +159,9 @@ export enum Action {
|
||||||
* Fired when joining a room failed
|
* Fired when joining a room failed
|
||||||
*/
|
*/
|
||||||
JoinRoomError = "join_room_error",
|
JoinRoomError = "join_room_error",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts content into the active composer. Should be used with ComposerInsertPayload
|
||||||
|
*/
|
||||||
|
ComposerInsert = "composer_insert",
|
||||||
}
|
}
|
||||||
|
|
42
src/dispatcher/payloads/ComposerInsertPayload.ts
Normal file
42
src/dispatcher/payloads/ComposerInsertPayload.ts
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
|
|
||||||
|
import { ActionPayload } from "../payloads";
|
||||||
|
import { Action } from "../actions";
|
||||||
|
|
||||||
|
interface IBaseComposerInsertPayload extends ActionPayload {
|
||||||
|
action: Action.ComposerInsert,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IComposerInsertMentionPayload extends IBaseComposerInsertPayload {
|
||||||
|
userId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IComposerInsertQuotePayload extends IBaseComposerInsertPayload {
|
||||||
|
event: MatrixEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IComposerInsertPlaintextPayload extends IBaseComposerInsertPayload {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ComposerInsertPayload =
|
||||||
|
IComposerInsertMentionPayload |
|
||||||
|
IComposerInsertQuotePayload |
|
||||||
|
IComposerInsertPlaintextPayload;
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
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 { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
import { RightPanelPhases } from "../../stores/RightPanelStorePhases";
|
||||||
import { ActionPayload } from "../payloads";
|
import { ActionPayload } from "../payloads";
|
||||||
import { Action } from "../actions";
|
import { Action } from "../actions";
|
||||||
|
@ -29,7 +30,7 @@ export interface SetRightPanelPhasePayload extends ActionPayload {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SetRightPanelPhaseRefireParams {
|
export interface SetRightPanelPhaseRefireParams {
|
||||||
member?: RoomMember;
|
member?: RoomMember | User;
|
||||||
verificationRequest?: VerificationRequest;
|
verificationRequest?: VerificationRequest;
|
||||||
groupId?: string;
|
groupId?: string;
|
||||||
groupRoomId?: string;
|
groupRoomId?: string;
|
||||||
|
|
|
@ -15,13 +15,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {diffAtCaret, diffDeletion, IDiff} from "./diff";
|
import { diffAtCaret, diffDeletion, IDiff } from "./diff";
|
||||||
import DocumentPosition, {IPosition} from "./position";
|
import DocumentPosition, { IPosition } from "./position";
|
||||||
import Range from "./range";
|
import Range from "./range";
|
||||||
import {SerializedPart, Part, PartCreator} from "./parts";
|
import { SerializedPart, Part, PartCreator } from "./parts";
|
||||||
import AutocompleteWrapperModel, {ICallback} from "./autocomplete";
|
import AutocompleteWrapperModel, { ICallback } from "./autocomplete";
|
||||||
import DocumentOffset from "./offset";
|
import DocumentOffset from "./offset";
|
||||||
import {Caret} from "./caret";
|
import { Caret } from "./caret";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @callback ModelCallback
|
* @callback ModelCallback
|
||||||
|
@ -390,7 +390,7 @@ export default class EditorModel {
|
||||||
return addLen;
|
return addLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
positionForOffset(totalOffset: number, atPartEnd: boolean) {
|
positionForOffset(totalOffset: number, atPartEnd = false) {
|
||||||
let currentOffset = 0;
|
let currentOffset = 0;
|
||||||
const index = this._parts.findIndex(part => {
|
const index = this._parts.findIndex(part => {
|
||||||
const partLen = part.text.length;
|
const partLen = part.text.length;
|
||||||
|
|
|
@ -21,11 +21,11 @@ import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
|
|
||||||
import {useEventEmitter} from "./useEventEmitter";
|
import {useEventEmitter} from "./useEventEmitter";
|
||||||
|
|
||||||
const tryGetContent = (ev?: MatrixEvent) => ev ? ev.getContent() : undefined;
|
const tryGetContent = <T extends {}>(ev?: MatrixEvent) => ev ? ev.getContent<T>() : undefined;
|
||||||
|
|
||||||
// Hook to simplify listening to Matrix account data
|
// Hook to simplify listening to Matrix account data
|
||||||
export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: string) => {
|
export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: string) => {
|
||||||
const [value, setValue] = useState<T>(() => tryGetContent(cli.getAccountData(eventType)));
|
const [value, setValue] = useState<T>(() => tryGetContent<T>(cli.getAccountData(eventType)));
|
||||||
|
|
||||||
const handler = useCallback((event) => {
|
const handler = useCallback((event) => {
|
||||||
if (event.getType() !== eventType) return;
|
if (event.getType() !== eventType) return;
|
||||||
|
@ -38,7 +38,7 @@ export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: strin
|
||||||
|
|
||||||
// Hook to simplify listening to Matrix room account data
|
// Hook to simplify listening to Matrix room account data
|
||||||
export const useRoomAccountData = <T extends {}>(room: Room, eventType: string) => {
|
export const useRoomAccountData = <T extends {}>(room: Room, eventType: string) => {
|
||||||
const [value, setValue] = useState<T>(() => tryGetContent(room.getAccountData(eventType)));
|
const [value, setValue] = useState<T>(() => tryGetContent<T>(room.getAccountData(eventType)));
|
||||||
|
|
||||||
const handler = useCallback((event) => {
|
const handler = useCallback((event) => {
|
||||||
if (event.getType() !== eventType) return;
|
if (event.getType() !== eventType) return;
|
||||||
|
|
|
@ -790,6 +790,7 @@
|
||||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||||
"Change notification settings": "Change notification settings",
|
"Change notification settings": "Change notification settings",
|
||||||
|
"Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators": "Report to moderators prototype. In rooms that support moderation, the `report` button will let you report abuse to room moderators",
|
||||||
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
|
"Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.": "Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. Requires compatible homeserver for some features.",
|
||||||
"Spaces": "Spaces",
|
"Spaces": "Spaces",
|
||||||
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
|
"Spaces are a new way to group rooms and people.": "Spaces are a new way to group rooms and people.",
|
||||||
|
@ -911,6 +912,8 @@
|
||||||
"Incoming voice call": "Incoming voice call",
|
"Incoming voice call": "Incoming voice call",
|
||||||
"Incoming video call": "Incoming video call",
|
"Incoming video call": "Incoming video call",
|
||||||
"Incoming call": "Incoming call",
|
"Incoming call": "Incoming call",
|
||||||
|
"Sound on": "Sound on",
|
||||||
|
"Silence call": "Silence call",
|
||||||
"Decline": "Decline",
|
"Decline": "Decline",
|
||||||
"Accept": "Accept",
|
"Accept": "Accept",
|
||||||
"Pause": "Pause",
|
"Pause": "Pause",
|
||||||
|
@ -2323,9 +2326,23 @@
|
||||||
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
"Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.": "Just a heads up, if you don't add an email and forget your password, you could <b>permanently lose access to your account</b>.",
|
||||||
"Email (optional)": "Email (optional)",
|
"Email (optional)": "Email (optional)",
|
||||||
"Please fill why you're reporting.": "Please fill why you're reporting.",
|
"Please fill why you're reporting.": "Please fill why you're reporting.",
|
||||||
|
"What this user is writing is wrong.\nThis will be reported to the room moderators.": "What this user is writing is wrong.\nThis will be reported to the room moderators.",
|
||||||
|
"This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.": "This user is displaying toxic behaviour, for instance by insulting other users or sharing adult-only content in a family-friendly room or otherwise violating the rules of this room.\nThis will be reported to the room moderators.",
|
||||||
|
"This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.": "This user is displaying illegal behaviour, for instance by doxing people or threatening violence.\nThis will be reported to the room moderators who may escalate this to legal authorities.",
|
||||||
|
"This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.": "This user is spamming the room with ads, links to ads or to propaganda.\nThis will be reported to the room moderators.",
|
||||||
|
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\nThis will be reported to the administrators of %(homeserver)s. The administrators will NOT be able to read the encrypted content of this room.",
|
||||||
|
"This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.": "This room is dedicated to illegal or toxic content or the moderators fail to moderate illegal or toxic content.\n This will be reported to the administrators of %(homeserver)s.",
|
||||||
|
"Any other reason. Please describe the problem.\nThis will be reported to the room moderators.": "Any other reason. Please describe the problem.\nThis will be reported to the room moderators.",
|
||||||
|
"Please pick a nature and describe what makes this message abusive.": "Please pick a nature and describe what makes this message abusive.",
|
||||||
|
"Report Content": "Report Content",
|
||||||
|
"Disagree": "Disagree",
|
||||||
|
"Toxic Behaviour": "Toxic Behaviour",
|
||||||
|
"Illegal Content": "Illegal Content",
|
||||||
|
"Spam or propaganda": "Spam or propaganda",
|
||||||
|
"Report the entire room": "Report the entire room",
|
||||||
|
"Send report": "Send report",
|
||||||
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
|
"Report Content to Your Homeserver Administrator": "Report Content to Your Homeserver Administrator",
|
||||||
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
|
"Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.": "Reporting this message will send its unique 'event ID' to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images.",
|
||||||
"Send report": "Send report",
|
|
||||||
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
|
"Room Settings - %(roomName)s": "Room Settings - %(roomName)s",
|
||||||
"Failed to upgrade room": "Failed to upgrade room",
|
"Failed to upgrade room": "Failed to upgrade room",
|
||||||
"The room upgrade could not be completed": "The room upgrade could not be completed",
|
"The room upgrade could not be completed": "The room upgrade could not be completed",
|
||||||
|
@ -2492,7 +2509,6 @@
|
||||||
"Share Message": "Share Message",
|
"Share Message": "Share Message",
|
||||||
"Source URL": "Source URL",
|
"Source URL": "Source URL",
|
||||||
"Collapse Reply Thread": "Collapse Reply Thread",
|
"Collapse Reply Thread": "Collapse Reply Thread",
|
||||||
"Report Content": "Report Content",
|
|
||||||
"Clear status": "Clear status",
|
"Clear status": "Clear status",
|
||||||
"Update status": "Update status",
|
"Update status": "Update status",
|
||||||
"Set status": "Set status",
|
"Set status": "Set status",
|
||||||
|
|
|
@ -300,7 +300,7 @@ export default class EventIndex extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private eventToJson(ev: MatrixEvent) {
|
private eventToJson(ev: MatrixEvent) {
|
||||||
const jsonEvent = ev.toJSON();
|
const jsonEvent: any = ev.toJSON();
|
||||||
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
|
const e = ev.isEncrypted() ? jsonEvent.decrypted : jsonEvent;
|
||||||
|
|
||||||
if (ev.isEncrypted()) {
|
if (ev.isEncrypted()) {
|
||||||
|
|
|
@ -37,17 +37,17 @@ export enum PerformanceEntryNames {
|
||||||
|
|
||||||
SWITCH_ROOM = "mx_SwithRoom",
|
SWITCH_ROOM = "mx_SwithRoom",
|
||||||
JUMP_TO_ROOM = "mx_JumpToRoom",
|
JUMP_TO_ROOM = "mx_JumpToRoom",
|
||||||
JOIN_ROOM = "mx_JoinRoom",
|
JOIN_ROOM = "mx_JoinRoom", // ✅
|
||||||
CREATE_DM = "mx_CreateDM",
|
CREATE_DM = "mx_CreateDM", // ✅
|
||||||
PEEK_ROOM = "mx_PeekRoom",
|
PEEK_ROOM = "mx_PeekRoom",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User
|
* User
|
||||||
*/
|
*/
|
||||||
|
|
||||||
VERIFY_E2EE_USER = "mx_VerifyE2EEUser",
|
VERIFY_E2EE_USER = "mx_VerifyE2EEUser", // ✅
|
||||||
LOGIN = "mx_Login",
|
LOGIN = "mx_Login", // ✅
|
||||||
REGISTER = "mx_Register",
|
REGISTER = "mx_Register", // ✅
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VoIP
|
* VoIP
|
||||||
|
|
|
@ -131,6 +131,13 @@ export interface ISetting {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SETTINGS: {[setting: string]: ISetting} = {
|
export const SETTINGS: {[setting: string]: ISetting} = {
|
||||||
|
"feature_report_to_moderators": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Report to moderators prototype. " +
|
||||||
|
"In rooms that support moderation, the `report` button will let you report abuse to room moderators"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"feature_spaces": {
|
"feature_spaces": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
|
displayName: _td("Spaces prototype. Incompatible with Communities, Communities v2 and Custom Tags. " +
|
||||||
|
|
|
@ -15,29 +15,42 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import { VerificationRequest } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
import { IKeyBackupVersion } from "matrix-js-sdk/src/crypto/keybackup";
|
||||||
|
import { ISecretStorageKeyInfo } from "matrix-js-sdk/src/matrix";
|
||||||
import { MatrixClientPeg } from '../MatrixClientPeg';
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
|
import { accessSecretStorage, AccessCancelledError } from '../SecurityManager';
|
||||||
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import { PHASE_DONE as VERIF_PHASE_DONE } from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
|
||||||
export const PHASE_LOADING = 0;
|
export enum Phase {
|
||||||
export const PHASE_INTRO = 1;
|
Loading = 0,
|
||||||
export const PHASE_BUSY = 2;
|
Intro = 1,
|
||||||
export const PHASE_DONE = 3; //final done stage, but still showing UX
|
Busy = 2,
|
||||||
export const PHASE_CONFIRM_SKIP = 4;
|
Done = 3, // final done stage, but still showing UX
|
||||||
export const PHASE_FINISHED = 5; //UX can be closed
|
ConfirmSkip = 4,
|
||||||
|
Finished = 5, // UX can be closed
|
||||||
|
}
|
||||||
|
|
||||||
export class SetupEncryptionStore extends EventEmitter {
|
export class SetupEncryptionStore extends EventEmitter {
|
||||||
static sharedInstance() {
|
private started: boolean;
|
||||||
if (!global.mx_SetupEncryptionStore) global.mx_SetupEncryptionStore = new SetupEncryptionStore();
|
public phase: Phase;
|
||||||
return global.mx_SetupEncryptionStore;
|
public verificationRequest: VerificationRequest;
|
||||||
|
public backupInfo: IKeyBackupVersion;
|
||||||
|
public keyId: string;
|
||||||
|
public keyInfo: ISecretStorageKeyInfo;
|
||||||
|
public hasDevicesToVerifyAgainst: boolean;
|
||||||
|
|
||||||
|
public static sharedInstance() {
|
||||||
|
if (!window.mxSetupEncryptionStore) window.mxSetupEncryptionStore = new SetupEncryptionStore();
|
||||||
|
return window.mxSetupEncryptionStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
public start(): void {
|
||||||
if (this._started) {
|
if (this.started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._started = true;
|
this.started = true;
|
||||||
this.phase = PHASE_LOADING;
|
this.phase = Phase.Loading;
|
||||||
this.verificationRequest = null;
|
this.verificationRequest = null;
|
||||||
this.backupInfo = null;
|
this.backupInfo = null;
|
||||||
|
|
||||||
|
@ -48,34 +61,34 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
cli.on("crypto.verification.request", this.onVerificationRequest);
|
cli.on("crypto.verification.request", this.onVerificationRequest);
|
||||||
cli.on('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
cli.on('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||||
|
|
||||||
const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
|
const requestsInProgress = cli.getVerificationRequestsToDeviceInProgress(cli.getUserId());
|
||||||
if (requestsInProgress.length) {
|
if (requestsInProgress.length) {
|
||||||
// If there are multiple, we take the most recent. Equally if the user sends another request from
|
// If there are multiple, we take the most recent. Equally if the user sends another request from
|
||||||
// another device after this screen has been shown, we'll switch to the new one, so this
|
// another device after this screen has been shown, we'll switch to the new one, so this
|
||||||
// generally doesn't support multiple requests.
|
// generally doesn't support multiple requests.
|
||||||
this._setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
|
this.setActiveVerificationRequest(requestsInProgress[requestsInProgress.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fetchKeyInfo();
|
this.fetchKeyInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
public stop(): void {
|
||||||
if (!this._started) {
|
if (!this.started) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._started = false;
|
this.started = false;
|
||||||
if (this.verificationRequest) {
|
if (this.verificationRequest) {
|
||||||
this.verificationRequest.off("change", this.onVerificationRequestChange);
|
this.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||||
}
|
}
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
|
MatrixClientPeg.get().removeListener("crypto.verification.request", this.onVerificationRequest);
|
||||||
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
|
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchKeyInfo() {
|
public async fetchKeyInfo(): Promise<void> {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
const keys = await cli.isSecretStored('m.cross_signing.master', false);
|
const keys = await cli.isSecretStored('m.cross_signing.master', false);
|
||||||
if (keys === null || Object.keys(keys).length === 0) {
|
if (keys === null || Object.keys(keys).length === 0) {
|
||||||
|
@ -97,15 +110,15 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
|
|
||||||
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
|
if (!this.hasDevicesToVerifyAgainst && !this.keyInfo) {
|
||||||
// skip before we can even render anything.
|
// skip before we can even render anything.
|
||||||
this.phase = PHASE_FINISHED;
|
this.phase = Phase.Finished;
|
||||||
} else {
|
} else {
|
||||||
this.phase = PHASE_INTRO;
|
this.phase = Phase.Intro;
|
||||||
}
|
}
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
async usePassPhrase() {
|
public async usePassPhrase(): Promise<void> {
|
||||||
this.phase = PHASE_BUSY;
|
this.phase = Phase.Busy;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
try {
|
try {
|
||||||
|
@ -120,7 +133,7 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
// passphase cached for that work. This dialog itself will only wait
|
// passphase cached for that work. This dialog itself will only wait
|
||||||
// on the first trust check, and the key backup restore will happen
|
// on the first trust check, and the key backup restore will happen
|
||||||
// in the background.
|
// in the background.
|
||||||
await new Promise((resolve, reject) => {
|
await new Promise((resolve: (value?: unknown) => void, reject: (reason?: any) => void) => {
|
||||||
accessSecretStorage(async () => {
|
accessSecretStorage(async () => {
|
||||||
await cli.checkOwnCrossSigningTrust();
|
await cli.checkOwnCrossSigningTrust();
|
||||||
resolve();
|
resolve();
|
||||||
|
@ -134,7 +147,7 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
});
|
});
|
||||||
|
|
||||||
if (cli.getCrossSigningId()) {
|
if (cli.getCrossSigningId()) {
|
||||||
this.phase = PHASE_DONE;
|
this.phase = Phase.Done;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -142,25 +155,25 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
// this will throw if the user hits cancel, so ignore
|
// this will throw if the user hits cancel, so ignore
|
||||||
this.phase = PHASE_INTRO;
|
this.phase = Phase.Intro;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onUserTrustStatusChanged = (userId) => {
|
private onUserTrustStatusChanged = (userId: string) => {
|
||||||
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
if (userId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
|
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
|
||||||
if (publicKeysTrusted) {
|
if (publicKeysTrusted) {
|
||||||
this.phase = PHASE_DONE;
|
this.phase = Phase.Done;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVerificationRequest = (request) => {
|
public onVerificationRequest = (request: VerificationRequest): void => {
|
||||||
this._setActiveVerificationRequest(request);
|
this.setActiveVerificationRequest(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
onVerificationRequestChange = () => {
|
public onVerificationRequestChange = (): void => {
|
||||||
if (this.verificationRequest.cancelled) {
|
if (this.verificationRequest.cancelled) {
|
||||||
this.verificationRequest.off("change", this.onVerificationRequestChange);
|
this.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||||
this.verificationRequest = null;
|
this.verificationRequest = null;
|
||||||
|
@ -172,34 +185,34 @@ export class SetupEncryptionStore extends EventEmitter {
|
||||||
// cross signing to be ready to use, so wait for the user trust status to
|
// cross signing to be ready to use, so wait for the user trust status to
|
||||||
// change (or change to DONE if it's already ready).
|
// change (or change to DONE if it's already ready).
|
||||||
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
|
const publicKeysTrusted = MatrixClientPeg.get().getCrossSigningId();
|
||||||
this.phase = publicKeysTrusted ? PHASE_DONE : PHASE_BUSY;
|
this.phase = publicKeysTrusted ? Phase.Done : Phase.Busy;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
skip() {
|
public skip(): void {
|
||||||
this.phase = PHASE_CONFIRM_SKIP;
|
this.phase = Phase.ConfirmSkip;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
skipConfirm() {
|
public skipConfirm(): void {
|
||||||
this.phase = PHASE_FINISHED;
|
this.phase = Phase.Finished;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
returnAfterSkip() {
|
public returnAfterSkip(): void {
|
||||||
this.phase = PHASE_INTRO;
|
this.phase = Phase.Intro;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
}
|
}
|
||||||
|
|
||||||
done() {
|
public done(): void {
|
||||||
this.phase = PHASE_FINISHED;
|
this.phase = Phase.Finished;
|
||||||
this.emit("update");
|
this.emit("update");
|
||||||
// async - ask other clients for keys, if necessary
|
// async - ask other clients for keys, if necessary
|
||||||
MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests();
|
MatrixClientPeg.get().crypto.cancelAndResendAllOutgoingKeyRequests();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _setActiveVerificationRequest(request) {
|
private async setActiveVerificationRequest(request: VerificationRequest): Promise<void> {
|
||||||
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
|
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
|
|
||||||
if (this.verificationRequest) {
|
if (this.verificationRequest) {
|
|
@ -16,8 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import { IWidget } from 'matrix-widget-api';
|
import { IWidget } from 'matrix-widget-api';
|
||||||
import MatrixEvent from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import {WidgetType} from "../widgets/WidgetType";
|
import { WidgetType } from "../widgets/WidgetType";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Acts as a place to get & set widget state, storing local echo state and
|
* Acts as a place to get & set widget state, storing local echo state and
|
||||||
|
|
|
@ -51,7 +51,7 @@ import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||||
import {getCustomTheme} from "../../theme";
|
import {getCustomTheme} from "../../theme";
|
||||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||||
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
import { MatrixEvent, IEvent } from "matrix-js-sdk/src/models/event";
|
||||||
import { ELEMENT_CLIENT_ID } from "../../identifiers";
|
import { ELEMENT_CLIENT_ID } from "../../identifiers";
|
||||||
import { getUserLanguage } from "../../languageHandler";
|
import { getUserLanguage } from "../../languageHandler";
|
||||||
|
|
||||||
|
@ -415,7 +415,7 @@ export class StopGapWidget extends EventEmitter {
|
||||||
private feedEvent(ev: MatrixEvent) {
|
private feedEvent(ev: MatrixEvent) {
|
||||||
if (!this.messaging) return;
|
if (!this.messaging) return;
|
||||||
|
|
||||||
const raw = ev.event;
|
const raw = ev.event as IEvent;
|
||||||
this.messaging.feedEvent(raw).catch(e => {
|
this.messaging.feedEvent(raw).catch(e => {
|
||||||
console.error("Error sending event to widget: ", e);
|
console.error("Error sending event to widget: ", e);
|
||||||
});
|
});
|
||||||
|
|
|
@ -145,7 +145,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
return {roomId, eventId: r.event_id};
|
return {roomId, eventId: r.event_id};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<MatrixEvent[]> {
|
public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise<object[]> {
|
||||||
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
|
limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -167,9 +167,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
return results.map(e => e.event);
|
return results.map(e => e.event);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async readStateEvents(
|
public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise<object[]> {
|
||||||
eventType: string, stateKey: string | undefined, limit: number,
|
|
||||||
): Promise<MatrixEvent[]> {
|
|
||||||
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
|
limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice
|
||||||
|
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
@ -178,7 +176,7 @@ export class StopGapWidgetDriver extends WidgetDriver {
|
||||||
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
|
if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client");
|
||||||
|
|
||||||
const results: MatrixEvent[] = [];
|
const results: MatrixEvent[] = [];
|
||||||
const state = room.currentState.events.get(eventType);
|
const state: Map<string, MatrixEvent> = room.currentState.events.get(eventType);
|
||||||
if (state) {
|
if (state) {
|
||||||
if (stateKey === "" || !!stateKey) {
|
if (stateKey === "" || !!stateKey) {
|
||||||
const forKey = state.get(stateKey);
|
const forKey = state.get(stateKey);
|
||||||
|
|
|
@ -21,7 +21,7 @@ import DeviceListener from '../DeviceListener';
|
||||||
import ToastStore from "../stores/ToastStore";
|
import ToastStore from "../stores/ToastStore";
|
||||||
import GenericToast from "../components/views/toasts/GenericToast";
|
import GenericToast from "../components/views/toasts/GenericToast";
|
||||||
import { Action } from "../dispatcher/actions";
|
import { Action } from "../dispatcher/actions";
|
||||||
import { USER_SECURITY_TAB } from "../components/views/dialogs/UserSettingsDialog";
|
import { UserTab } from "../components/views/dialogs/UserSettingsDialog";
|
||||||
|
|
||||||
function toastKey(deviceId: string) {
|
function toastKey(deviceId: string) {
|
||||||
return "unverified_session_" + deviceId;
|
return "unverified_session_" + deviceId;
|
||||||
|
@ -34,7 +34,7 @@ export const showToast = async (deviceId: string) => {
|
||||||
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
|
DeviceListener.sharedInstance().dismissUnverifiedSessions([deviceId]);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: Action.ViewUserSettings,
|
action: Action.ViewUserSettings,
|
||||||
initialTabId: USER_SECURITY_TAB,
|
initialTabId: UserTab.Security,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import { uniq } from "lodash";
|
||||||
import {uniq} from "lodash";
|
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 {Event} from "matrix-js-sdk/src/models/event";
|
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
import { MatrixClientPeg } from '../MatrixClientPeg';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that takes a Matrix Client and flips the m.direct map
|
* Class that takes a Matrix Client and flips the m.direct map
|
||||||
|
@ -30,15 +30,13 @@ import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||||
export default class DMRoomMap {
|
export default class DMRoomMap {
|
||||||
private static sharedInstance: DMRoomMap;
|
private static sharedInstance: DMRoomMap;
|
||||||
|
|
||||||
private matrixClient: MatrixClient;
|
|
||||||
// TODO: convert these to maps
|
// TODO: convert these to maps
|
||||||
private roomToUser: {[key: string]: string} = null;
|
private roomToUser: {[key: string]: string} = null;
|
||||||
private userToRooms: {[key: string]: string[]} = null;
|
private userToRooms: {[key: string]: string[]} = null;
|
||||||
private hasSentOutPatchDirectAccountDataPatch: boolean;
|
private hasSentOutPatchDirectAccountDataPatch: boolean;
|
||||||
private mDirectEvent: Event;
|
private mDirectEvent: object;
|
||||||
|
|
||||||
constructor(matrixClient) {
|
constructor(private readonly matrixClient: MatrixClient) {
|
||||||
this.matrixClient = matrixClient;
|
|
||||||
// see onAccountData
|
// see onAccountData
|
||||||
this.hasSentOutPatchDirectAccountDataPatch = false;
|
this.hasSentOutPatchDirectAccountDataPatch = false;
|
||||||
|
|
||||||
|
|
|
@ -392,7 +392,7 @@ export default class WidgetUtils {
|
||||||
}
|
}
|
||||||
const widgets = client.getAccountData('m.widgets');
|
const widgets = client.getAccountData('m.widgets');
|
||||||
if (!widgets) return;
|
if (!widgets) return;
|
||||||
const userWidgets: IWidgetEvent[] = widgets.getContent() || {};
|
const userWidgets: Record<string, IWidgetEvent> = widgets.getContent() || {};
|
||||||
Object.entries(userWidgets).forEach(([key, widget]) => {
|
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||||
if (widget.content && widget.content.type === "m.integration_manager") {
|
if (widget.content && widget.content.type === "m.integration_manager") {
|
||||||
delete userWidgets[key];
|
delete userWidgets[key];
|
||||||
|
|
|
@ -30,9 +30,7 @@ function createFailedDecryptionEvent() {
|
||||||
const event = new MatrixEvent({
|
const event = new MatrixEvent({
|
||||||
event_id: "event-id-" + Math.random().toString(16).slice(2),
|
event_id: "event-id-" + Math.random().toString(16).slice(2),
|
||||||
});
|
});
|
||||||
event._setClearData(
|
event.setClearData(event.badEncryptedMessage(":("));
|
||||||
event._badEncryptedMessage(":("),
|
|
||||||
);
|
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +65,7 @@ describe('DecryptionFailureTracker', function() {
|
||||||
tracker.eventDecrypted(decryptedEvent, err);
|
tracker.eventDecrypted(decryptedEvent, err);
|
||||||
|
|
||||||
// Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted
|
// Indicate successful decryption: clear data can be anything where the msgtype is not m.bad.encrypted
|
||||||
decryptedEvent._setClearData({});
|
decryptedEvent.setClearData({});
|
||||||
tracker.eventDecrypted(decryptedEvent, null);
|
tracker.eventDecrypted(decryptedEvent, null);
|
||||||
|
|
||||||
// Pretend "now" is Infinity
|
// Pretend "now" is Infinity
|
||||||
|
|
|
@ -20,9 +20,11 @@ const acceptInvite = require('../usecases/accept-invite');
|
||||||
const {receiveMessage} = require('../usecases/timeline');
|
const {receiveMessage} = require('../usecases/timeline');
|
||||||
const {createDm} = require('../usecases/create-room');
|
const {createDm} = require('../usecases/create-room');
|
||||||
const {checkRoomSettings} = require('../usecases/room-settings');
|
const {checkRoomSettings} = require('../usecases/room-settings');
|
||||||
const {startSasVerifcation, acceptSasVerification} = require('../usecases/verify');
|
const {startSasVerification, acceptSasVerification} = require('../usecases/verify');
|
||||||
const { setupSecureBackup } = require('../usecases/security');
|
const { setupSecureBackup } = require('../usecases/security');
|
||||||
const assert = require('assert');
|
const assert = require('assert');
|
||||||
|
const { measureStart, measureStop } = require('../util');
|
||||||
|
|
||||||
|
|
||||||
module.exports = async function e2eEncryptionScenarios(alice, bob) {
|
module.exports = async function e2eEncryptionScenarios(alice, bob) {
|
||||||
console.log(" creating an e2e encrypted DM and join through invite:");
|
console.log(" creating an e2e encrypted DM and join through invite:");
|
||||||
|
@ -31,12 +33,14 @@ module.exports = async function e2eEncryptionScenarios(alice, bob) {
|
||||||
await acceptInvite(alice, 'bob');
|
await acceptInvite(alice, 'bob');
|
||||||
// do sas verifcation
|
// do sas verifcation
|
||||||
bob.log.step(`starts SAS verification with ${alice.username}`);
|
bob.log.step(`starts SAS verification with ${alice.username}`);
|
||||||
const bobSasPromise = startSasVerifcation(bob, alice.username);
|
await measureStart(bob, "mx_VerifyE2EEUser");
|
||||||
|
const bobSasPromise = startSasVerification(bob, alice.username);
|
||||||
const aliceSasPromise = acceptSasVerification(alice, bob.username);
|
const aliceSasPromise = acceptSasVerification(alice, bob.username);
|
||||||
// wait in parallel, so they don't deadlock on each other
|
// wait in parallel, so they don't deadlock on each other
|
||||||
// the logs get a bit messy here, but that's fine enough for debugging (hopefully)
|
// the logs get a bit messy here, but that's fine enough for debugging (hopefully)
|
||||||
const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]);
|
const [bobSas, aliceSas] = await Promise.all([bobSasPromise, aliceSasPromise]);
|
||||||
assert.deepEqual(bobSas, aliceSas);
|
assert.deepEqual(bobSas, aliceSas);
|
||||||
|
await measureStop(bob, "mx_VerifyE2EEUser");
|
||||||
bob.log.done(`done (match for ${bobSas.join(", ")})`);
|
bob.log.done(`done (match for ${bobSas.join(", ")})`);
|
||||||
const aliceMessage = "Guess what I just heard?!";
|
const aliceMessage = "Guess what I just heard?!";
|
||||||
await sendMessage(alice, aliceMessage);
|
await sendMessage(alice, aliceMessage);
|
||||||
|
|
|
@ -15,6 +15,8 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const { measureStart, measureStop } = require('../util');
|
||||||
|
|
||||||
async function openRoomDirectory(session) {
|
async function openRoomDirectory(session) {
|
||||||
const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
|
const roomDirectoryButton = await session.query('.mx_LeftPanel_exploreButton');
|
||||||
await roomDirectoryButton.click();
|
await roomDirectoryButton.click();
|
||||||
|
@ -52,6 +54,8 @@ async function createRoom(session, roomName, encrypted=false) {
|
||||||
async function createDm(session, invitees) {
|
async function createDm(session, invitees) {
|
||||||
session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
|
session.log.step(`creates DM with ${JSON.stringify(invitees)}`);
|
||||||
|
|
||||||
|
await measureStart(session, "mx_CreateDM");
|
||||||
|
|
||||||
const dmsSublist = await findSublist(session, "people");
|
const dmsSublist = await findSublist(session, "people");
|
||||||
const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton");
|
const startChatButton = await dmsSublist.$(".mx_RoomSublist_auxButton");
|
||||||
await startChatButton.click();
|
await startChatButton.click();
|
||||||
|
@ -76,6 +80,8 @@ async function createDm(session, invitees) {
|
||||||
|
|
||||||
await session.query('.mx_MessageComposer');
|
await session.query('.mx_MessageComposer');
|
||||||
session.log.done();
|
session.log.done();
|
||||||
|
|
||||||
|
await measureStop(session, "mx_CreateDM");
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {openRoomDirectory, findSublist, createRoom, createDm};
|
module.exports = {openRoomDirectory, findSublist, createRoom, createDm};
|
||||||
|
|
|
@ -16,9 +16,12 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const {openRoomDirectory} = require('./create-room');
|
const {openRoomDirectory} = require('./create-room');
|
||||||
|
const { measureStart, measureStop } = require('../util');
|
||||||
|
|
||||||
|
|
||||||
module.exports = async function join(session, roomName) {
|
module.exports = async function join(session, roomName) {
|
||||||
session.log.step(`joins room "${roomName}"`);
|
session.log.step(`joins room "${roomName}"`);
|
||||||
|
await measureStart(session, "mx_JoinRoom");
|
||||||
await openRoomDirectory(session);
|
await openRoomDirectory(session);
|
||||||
const roomInput = await session.query('.mx_DirectorySearchBox input');
|
const roomInput = await session.query('.mx_DirectorySearchBox input');
|
||||||
await session.replaceInputText(roomInput, roomName);
|
await session.replaceInputText(roomInput, roomName);
|
||||||
|
@ -26,5 +29,6 @@ module.exports = async function join(session, roomName) {
|
||||||
const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton');
|
const joinFirstLink = await session.query('.mx_RoomDirectory_table .mx_RoomDirectory_join .mx_AccessibleButton');
|
||||||
await joinFirstLink.click();
|
await joinFirstLink.click();
|
||||||
await session.query('.mx_MessageComposer');
|
await session.query('.mx_MessageComposer');
|
||||||
|
await measureStop(session, "mx_JoinRoom");
|
||||||
session.log.done();
|
session.log.done();
|
||||||
};
|
};
|
||||||
|
|
|
@ -74,7 +74,7 @@ async function doSasVerification(session) {
|
||||||
return sasCodes;
|
return sasCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports.startSasVerifcation = async function(session, name) {
|
module.exports.startSasVerification = async function(session, name) {
|
||||||
session.log.startGroup("starts verification");
|
session.log.startGroup("starts verification");
|
||||||
await startVerification(session, name);
|
await startVerification(session, name);
|
||||||
|
|
||||||
|
|
|
@ -26,3 +26,15 @@ module.exports.range = function(start, amount, step = 1) {
|
||||||
module.exports.delay = function(ms) {
|
module.exports.delay = function(ms) {
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.measureStart = function(session, name) {
|
||||||
|
return session.page.evaluate(_name => {
|
||||||
|
window.mxPerformanceMonitor.start(_name);
|
||||||
|
}, name);
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports.measureStop = function(session, name) {
|
||||||
|
return session.page.evaluate(_name => {
|
||||||
|
window.mxPerformanceMonitor.stop(_name);
|
||||||
|
}, name);
|
||||||
|
};
|
||||||
|
|
|
@ -88,6 +88,10 @@ async function runTests() {
|
||||||
window.mxPerformanceMonitor.addPerformanceDataCallback({
|
window.mxPerformanceMonitor.addPerformanceDataCallback({
|
||||||
entryNames: [
|
entryNames: [
|
||||||
window.mxPerformanceEntryNames.REGISTER,
|
window.mxPerformanceEntryNames.REGISTER,
|
||||||
|
window.mxPerformanceEntryNames.LOGIN,
|
||||||
|
window.mxPerformanceEntryNames.JOIN_ROOM,
|
||||||
|
window.mxPerformanceEntryNames.CREATE_DM,
|
||||||
|
window.mxPerformanceEntryNames.VERIFY_E2EE_USER,
|
||||||
],
|
],
|
||||||
callback: (events) => {
|
callback: (events) => {
|
||||||
measurements = JSON.stringify(events);
|
measurements = JSON.stringify(events);
|
||||||
|
|
|
@ -5738,9 +5738,10 @@ mathml-tag-names@^2.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
|
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3"
|
||||||
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
|
||||||
|
|
||||||
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
|
matrix-js-sdk@12.0.0:
|
||||||
version "11.2.0"
|
version "12.0.0"
|
||||||
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/35ecbed29d16982deff27a8c37b05167738225a2"
|
resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.0.0.tgz#8ee7cc37661476341d0c792a1a12bc78b19f9fdd"
|
||||||
|
integrity sha512-DHeq87Sx9Dv37FYyvZkmA1VYsQUNaVgc3QzMUkFwoHt1T4EZzgyYpdsp3uYruJzUW0ACvVJcwFdrU4e1VS97dQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/runtime" "^7.12.5"
|
"@babel/runtime" "^7.12.5"
|
||||||
another-json "^0.2.0"
|
another-json "^0.2.0"
|
||||||
|
|
Loading…
Reference in a new issue