mirror of
https://github.com/vector-im/element-web.git
synced 2024-11-16 21:24:59 +08:00
Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into mac-redo
This commit is contained in:
commit
038888f2d4
@ -1,119 +0,0 @@
|
||||
steps:
|
||||
- label: ":eslint: JS Lint"
|
||||
command:
|
||||
# We fetch the develop js-sdk to get our latest eslint rules
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- "echo '+++ Lint'"
|
||||
- "yarn lint:js"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
|
||||
- label: ":eslint: TS Lint"
|
||||
command:
|
||||
- "echo '--- Install'"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "echo '+++ Lint'"
|
||||
- "yarn lint:ts"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
|
||||
- label: ":eslint: Types Lint"
|
||||
command:
|
||||
- "echo '--- Install'"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "echo '+++ Lint'"
|
||||
- "yarn lint:types"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
- label: ":stylelint: Style Lint"
|
||||
command:
|
||||
- "echo '--- Install'"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "yarn lint:style"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
|
||||
- label: ":jest: Tests"
|
||||
agents:
|
||||
# We use a medium sized instance instead of the normal small ones because
|
||||
# webpack loves to gorge itself on resources.
|
||||
queue: "medium"
|
||||
command:
|
||||
- "echo '--- Install js-sdk'"
|
||||
# We don't use the babel-ed output for anything so we can --ignore-scripts
|
||||
# to save transpiling the files. We run the transpile step explicitly in
|
||||
# the 'build' job.
|
||||
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- "yarn run reskindex"
|
||||
- "echo '+++ Running Tests'"
|
||||
- "yarn test"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
|
||||
- label: "🛠 Build"
|
||||
command:
|
||||
- "echo '+++ Install & Build'"
|
||||
- "yarn install"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:12"
|
||||
|
||||
- label: ":chains: End-to-End Tests"
|
||||
agents:
|
||||
# We use a xlarge sized instance instead of the normal small ones because
|
||||
# e2e tests otherwise take +-8min
|
||||
queue: "xlarge"
|
||||
command:
|
||||
- "echo '--- Install js-sdk'"
|
||||
- "./scripts/ci/install-deps.sh --ignore-scripts"
|
||||
- "echo '+++ Running Tests'"
|
||||
- "./scripts/ci/end-to-end-tests.sh"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "matrixdotorg/riotweb-ci-e2etests-env:latest"
|
||||
propagate-environment: true
|
||||
workdir: "/workdir/matrix-react-sdk"
|
||||
retry:
|
||||
automatic:
|
||||
- exit_status: 1 # retry end-to-end tests once as Puppeteer sometimes fails
|
||||
limit: 1
|
||||
|
||||
- label: "🔧 Riot Tests"
|
||||
agents:
|
||||
# We use a medium sized instance instead of the normal small ones because
|
||||
# webpack loves to gorge itself on resources.
|
||||
queue: "medium"
|
||||
command:
|
||||
- "echo '+++ Running Tests'"
|
||||
- "./scripts/ci/riot-unit-tests.sh"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
propagate-environment: true
|
||||
workdir: "/workdir/matrix-react-sdk"
|
||||
|
||||
- label: "🌐 i18n"
|
||||
command:
|
||||
- "echo '--- Fetching Dependencies'"
|
||||
- "yarn install --ignore-scripts"
|
||||
- "echo '+++ Testing i18n output'"
|
||||
- "yarn diff-i18n"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- wait
|
||||
|
||||
- label: "🐴 Trigger riot-web"
|
||||
trigger: "riot-web"
|
||||
branches: "develop"
|
||||
build:
|
||||
branch: "develop"
|
||||
message: "[react-sdk] ${BUILDKITE_MESSAGE}"
|
||||
async: true
|
@ -1 +1,4 @@
|
||||
src/component-index.js
|
||||
test/end-to-end-tests/node_modules/
|
||||
test/end-to-end-tests/riot/
|
||||
test/end-to-end-tests/synapse/
|
||||
|
@ -61,3 +61,7 @@ test/mock-clock.js
|
||||
test/notifications/ContentRules-test.js
|
||||
test/notifications/PushRuleVectorState-test.js
|
||||
test/stores/RoomViewStore-test.js
|
||||
src/component-index.js
|
||||
test/end-to-end-tests/node_modules/
|
||||
test/end-to-end-tests/riot/
|
||||
test/end-to-end-tests/synapse/
|
||||
|
251
CHANGELOG.md
251
CHANGELOG.md
@ -1,3 +1,254 @@
|
||||
Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3)
|
||||
|
||||
* Upgrade JS SDK to 5.1.1
|
||||
* Add default on config setting to control call button in composer
|
||||
[\#4228](https://github.com/matrix-org/matrix-react-sdk/pull/4228)
|
||||
* Fix: make alternative addresses UX less confusing
|
||||
[\#4226](https://github.com/matrix-org/matrix-react-sdk/pull/4226)
|
||||
* Fix: best-effort to join room without canonical alias over federation from
|
||||
room directory
|
||||
[\#4211](https://github.com/matrix-org/matrix-react-sdk/pull/4211)
|
||||
|
||||
Changes in [2.2.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3-rc.1) (2020-03-11)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.1...v2.2.3-rc.1)
|
||||
|
||||
* Update from Weblate
|
||||
[\#4200](https://github.com/matrix-org/matrix-react-sdk/pull/4200)
|
||||
* Revert "enable 4s when accepting a verification request"
|
||||
[\#4198](https://github.com/matrix-org/matrix-react-sdk/pull/4198)
|
||||
* Don't remount main split children on rhs collapse
|
||||
[\#4197](https://github.com/matrix-org/matrix-react-sdk/pull/4197)
|
||||
* Add fallback label for canonical alias events that dont change anything
|
||||
[\#4195](https://github.com/matrix-org/matrix-react-sdk/pull/4195)
|
||||
* Immediately switch to verification dialog when clicking [Continue] from new
|
||||
session dialog
|
||||
[\#4196](https://github.com/matrix-org/matrix-react-sdk/pull/4196)
|
||||
* Enable 4S if needed when trying to verify or accepting verification
|
||||
[\#4194](https://github.com/matrix-org/matrix-react-sdk/pull/4194)
|
||||
* Remove extraneous tab stop from room tree view.
|
||||
[\#4193](https://github.com/matrix-org/matrix-react-sdk/pull/4193)
|
||||
* Remove v1 identity server fallbacks
|
||||
[\#4191](https://github.com/matrix-org/matrix-react-sdk/pull/4191)
|
||||
* Allow editing of alt_aliases according to MSC2432
|
||||
[\#4187](https://github.com/matrix-org/matrix-react-sdk/pull/4187)
|
||||
* Update timeline rendering of aliases
|
||||
[\#4189](https://github.com/matrix-org/matrix-react-sdk/pull/4189)
|
||||
* Fix mark as read button for dark theme
|
||||
[\#4190](https://github.com/matrix-org/matrix-react-sdk/pull/4190)
|
||||
* Un-linkify version in settings
|
||||
[\#4188](https://github.com/matrix-org/matrix-react-sdk/pull/4188)
|
||||
* Make Mjolnir stop more robust
|
||||
[\#4186](https://github.com/matrix-org/matrix-react-sdk/pull/4186)
|
||||
* Fix secret sharing names to match spec
|
||||
[\#4185](https://github.com/matrix-org/matrix-react-sdk/pull/4185)
|
||||
* Share secrets with another device on request
|
||||
[\#4172](https://github.com/matrix-org/matrix-react-sdk/pull/4172)
|
||||
* Fall back to to_device verification if other user hasn't uploaded cross-
|
||||
signing keys
|
||||
[\#4181](https://github.com/matrix-org/matrix-react-sdk/pull/4181)
|
||||
* Disable edits on redacted events
|
||||
[\#4182](https://github.com/matrix-org/matrix-react-sdk/pull/4182)
|
||||
* Use crypto.verification.request even when xsign is disabled
|
||||
[\#4180](https://github.com/matrix-org/matrix-react-sdk/pull/4180)
|
||||
* Reword the status for the currently indexing rooms.
|
||||
[\#4084](https://github.com/matrix-org/matrix-react-sdk/pull/4084)
|
||||
* Moved read receipts to the bottom of the message
|
||||
[\#3892](https://github.com/matrix-org/matrix-react-sdk/pull/3892)
|
||||
* Include a mark as read X under the scroll to unread button
|
||||
[\#4159](https://github.com/matrix-org/matrix-react-sdk/pull/4159)
|
||||
* Show the room presence indicator, even when cross-singing is enabled
|
||||
[\#4178](https://github.com/matrix-org/matrix-react-sdk/pull/4178)
|
||||
* Add local echo when clicking "Manually Verify" in unverified session dialog
|
||||
[\#4179](https://github.com/matrix-org/matrix-react-sdk/pull/4179)
|
||||
* link to matrix.org/security-disclosure-policy in help screen
|
||||
[\#4129](https://github.com/matrix-org/matrix-react-sdk/pull/4129)
|
||||
* only show verify button if user has uploaded cross-signing keys
|
||||
[\#4174](https://github.com/matrix-org/matrix-react-sdk/pull/4174)
|
||||
* Fix room alias references in topics
|
||||
[\#4176](https://github.com/matrix-org/matrix-react-sdk/pull/4176)
|
||||
* Fix not being able to start chats when you have no rooms
|
||||
[\#4177](https://github.com/matrix-org/matrix-react-sdk/pull/4177)
|
||||
* Disable registration flows on SSO servers
|
||||
[\#4170](https://github.com/matrix-org/matrix-react-sdk/pull/4170)
|
||||
* Don't group blank membership changes
|
||||
[\#4160](https://github.com/matrix-org/matrix-react-sdk/pull/4160)
|
||||
* Ensure the room list always triggers updates on itself
|
||||
[\#4175](https://github.com/matrix-org/matrix-react-sdk/pull/4175)
|
||||
* Fix composer touch bar flickering on keypress in Chrome
|
||||
[\#4173](https://github.com/matrix-org/matrix-react-sdk/pull/4173)
|
||||
* Document scrollpanel and BACAT scrolling
|
||||
[\#4167](https://github.com/matrix-org/matrix-react-sdk/pull/4167)
|
||||
* riot-desktop open SSO in browser so user doesn't have to auth twice
|
||||
[\#4158](https://github.com/matrix-org/matrix-react-sdk/pull/4158)
|
||||
* Lock login and registration buttons after submit
|
||||
[\#4165](https://github.com/matrix-org/matrix-react-sdk/pull/4165)
|
||||
* Suggest the server's results as lower quality in the invite dialog
|
||||
[\#4149](https://github.com/matrix-org/matrix-react-sdk/pull/4149)
|
||||
* Adjust scroll offset with relative scrolling
|
||||
[\#4166](https://github.com/matrix-org/matrix-react-sdk/pull/4166)
|
||||
* only automatically download in usercontent if user requested it
|
||||
[\#4163](https://github.com/matrix-org/matrix-react-sdk/pull/4163)
|
||||
* Fix having to decrypt & download in two steps
|
||||
[\#4162](https://github.com/matrix-org/matrix-react-sdk/pull/4162)
|
||||
* Use bash for release script
|
||||
[\#4161](https://github.com/matrix-org/matrix-react-sdk/pull/4161)
|
||||
* Revert to manual sorting for custom tag rooms
|
||||
[\#4157](https://github.com/matrix-org/matrix-react-sdk/pull/4157)
|
||||
* Fix the last char of people's names being cut off in the invite dialog
|
||||
[\#4150](https://github.com/matrix-org/matrix-react-sdk/pull/4150)
|
||||
* Add /whois SlashCommand to open UserInfo
|
||||
[\#4154](https://github.com/matrix-org/matrix-react-sdk/pull/4154)
|
||||
* word-break in pills and wrap the background correctly
|
||||
[\#4155](https://github.com/matrix-org/matrix-react-sdk/pull/4155)
|
||||
* don't show "This alias is available to use" if the alias is invalid
|
||||
[\#4153](https://github.com/matrix-org/matrix-react-sdk/pull/4153)
|
||||
* Don't ask to enable analytics when Do Not Track is enabled
|
||||
[\#4098](https://github.com/matrix-org/matrix-react-sdk/pull/4098)
|
||||
* Fix MELS not breaking on day boundaries regression
|
||||
[\#4152](https://github.com/matrix-org/matrix-react-sdk/pull/4152)
|
||||
* Fix Quote on search results page
|
||||
[\#4151](https://github.com/matrix-org/matrix-react-sdk/pull/4151)
|
||||
* Ensure errors when creating a DM are raised to the user
|
||||
[\#4144](https://github.com/matrix-org/matrix-react-sdk/pull/4144)
|
||||
* Add a Login button to startAnyRegistrationFlow
|
||||
[\#3829](https://github.com/matrix-org/matrix-react-sdk/pull/3829)
|
||||
* Use latest backup status directly rather than via state
|
||||
[\#4147](https://github.com/matrix-org/matrix-react-sdk/pull/4147)
|
||||
* Prefer account password variation of upgrading
|
||||
[\#4146](https://github.com/matrix-org/matrix-react-sdk/pull/4146)
|
||||
* Hide user avatars from screen readers in group and room user lists.
|
||||
[\#4145](https://github.com/matrix-org/matrix-react-sdk/pull/4145)
|
||||
* Room List sorting algorithms
|
||||
[\#4085](https://github.com/matrix-org/matrix-react-sdk/pull/4085)
|
||||
* Clear selected tags when disabling tag panel
|
||||
[\#4143](https://github.com/matrix-org/matrix-react-sdk/pull/4143)
|
||||
* Ignore cursor jumping shortcuts with shift
|
||||
[\#4142](https://github.com/matrix-org/matrix-react-sdk/pull/4142)
|
||||
* add local echo for clicking 'start verification' button
|
||||
[\#4138](https://github.com/matrix-org/matrix-react-sdk/pull/4138)
|
||||
* Fix formatting buttons not marking the composer as modified
|
||||
[\#4141](https://github.com/matrix-org/matrix-react-sdk/pull/4141)
|
||||
* Upgrade deps
|
||||
[\#4136](https://github.com/matrix-org/matrix-react-sdk/pull/4136)
|
||||
* Remove debug line from Analytics
|
||||
[\#4137](https://github.com/matrix-org/matrix-react-sdk/pull/4137)
|
||||
* Use the right function for creating binary verification QR codes
|
||||
[\#4140](https://github.com/matrix-org/matrix-react-sdk/pull/4140)
|
||||
* Ensure verification QR codes use the right buffer size
|
||||
[\#4139](https://github.com/matrix-org/matrix-react-sdk/pull/4139)
|
||||
* Don't prefix QR codes with the length of the static marker string
|
||||
[\#4128](https://github.com/matrix-org/matrix-react-sdk/pull/4128)
|
||||
* Solve fixed-width digit display in flowed text
|
||||
[\#4127](https://github.com/matrix-org/matrix-react-sdk/pull/4127)
|
||||
* Limit UserInfo Displayname to 3 lines to get rid of scrollbars
|
||||
[\#4135](https://github.com/matrix-org/matrix-react-sdk/pull/4135)
|
||||
|
||||
Changes in [2.2.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.1) (2020-03-04)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.0...v2.2.1)
|
||||
|
||||
* Adjust scroll offset with relative scrolling
|
||||
[\#4171](https://github.com/matrix-org/matrix-react-sdk/pull/4171)
|
||||
* Disable registration flows on SSO servers
|
||||
[\#4169](https://github.com/matrix-org/matrix-react-sdk/pull/4169)
|
||||
|
||||
Changes in [2.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.0) (2020-03-02)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.0-rc.1...v2.2.0)
|
||||
|
||||
* Upgrade JS SDK to 5.1.0
|
||||
* Ignore cursor jumping shortcuts with shift
|
||||
[\#4142](https://github.com/matrix-org/matrix-react-sdk/pull/4142)
|
||||
|
||||
Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.0-rc.1) (2020-02-26)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.1...v2.2.0-rc.1)
|
||||
|
||||
* Upgrade JS SDK to 5.1.0-rc.1
|
||||
* Fix message context menu breaking on invalid m.room.pinned_events event
|
||||
[\#4133](https://github.com/matrix-org/matrix-react-sdk/pull/4133)
|
||||
* Update from Weblate
|
||||
[\#4134](https://github.com/matrix-org/matrix-react-sdk/pull/4134)
|
||||
* Notify platform of language changes
|
||||
[\#4121](https://github.com/matrix-org/matrix-react-sdk/pull/4121)
|
||||
* Handle errors when previewing rooms more safely
|
||||
[\#4132](https://github.com/matrix-org/matrix-react-sdk/pull/4132)
|
||||
* Don't try to collapse zero events with a group
|
||||
[\#4131](https://github.com/matrix-org/matrix-react-sdk/pull/4131)
|
||||
* Don't print errors when the tab is used with no autocomplete present
|
||||
[\#4130](https://github.com/matrix-org/matrix-react-sdk/pull/4130)
|
||||
* Improve UI feedback while waiting for network
|
||||
[\#4126](https://github.com/matrix-org/matrix-react-sdk/pull/4126)
|
||||
* Ensure DMs tagged outside of account data work in the invite dialog
|
||||
[\#4123](https://github.com/matrix-org/matrix-react-sdk/pull/4123)
|
||||
* Show a warning dialog when user indicates a new session wasn't them
|
||||
[\#4125](https://github.com/matrix-org/matrix-react-sdk/pull/4125)
|
||||
* Show cancel events as hidden events if we wouldn't usually render them
|
||||
[\#4120](https://github.com/matrix-org/matrix-react-sdk/pull/4120)
|
||||
* Collapsed room list has unaligned room tiles #4030 version 2
|
||||
[\#4033](https://github.com/matrix-org/matrix-react-sdk/pull/4033)
|
||||
* Check for cross-signing homeserver support
|
||||
[\#4118](https://github.com/matrix-org/matrix-react-sdk/pull/4118)
|
||||
* Don't leak if show_sas never comes (or already came)
|
||||
[\#4119](https://github.com/matrix-org/matrix-react-sdk/pull/4119)
|
||||
* Add verification request viewer in devtools
|
||||
[\#4106](https://github.com/matrix-org/matrix-react-sdk/pull/4106)
|
||||
* update phase when request prop changes
|
||||
[\#4117](https://github.com/matrix-org/matrix-react-sdk/pull/4117)
|
||||
* Handle file downloading locally in electron rather than sending to browser
|
||||
[\#4113](https://github.com/matrix-org/matrix-react-sdk/pull/4113)
|
||||
* Remove unused CIDER setting watcher
|
||||
[\#4116](https://github.com/matrix-org/matrix-react-sdk/pull/4116)
|
||||
* Use alt_aliases for pills and autocomplete
|
||||
[\#4102](https://github.com/matrix-org/matrix-react-sdk/pull/4102)
|
||||
* Add shortcuts for beginning / end of composer
|
||||
[\#4108](https://github.com/matrix-org/matrix-react-sdk/pull/4108)
|
||||
* Update from Weblate
|
||||
[\#4115](https://github.com/matrix-org/matrix-react-sdk/pull/4115)
|
||||
* Revert "Fix escaped markdown passing backslashes through"
|
||||
[\#4114](https://github.com/matrix-org/matrix-react-sdk/pull/4114)
|
||||
* Fix a couple of React warnings/errors
|
||||
[\#4112](https://github.com/matrix-org/matrix-react-sdk/pull/4112)
|
||||
* Fix two big DOM leaks which were locking Chrome solid.
|
||||
[\#4111](https://github.com/matrix-org/matrix-react-sdk/pull/4111)
|
||||
* Filter out empty strings when pasting IDs into the invite dialog
|
||||
[\#4109](https://github.com/matrix-org/matrix-react-sdk/pull/4109)
|
||||
* Remove buildkite pipeline
|
||||
[\#4107](https://github.com/matrix-org/matrix-react-sdk/pull/4107)
|
||||
* Use binary packing for verification QR codes
|
||||
[\#4091](https://github.com/matrix-org/matrix-react-sdk/pull/4091)
|
||||
* Fix several small bugs with the invite/DM dialog
|
||||
[\#4099](https://github.com/matrix-org/matrix-react-sdk/pull/4099)
|
||||
* ignore e2e tests node_modules during linting
|
||||
[\#4103](https://github.com/matrix-org/matrix-react-sdk/pull/4103)
|
||||
* Apply null-guard to room pills for when we can't fetch the room
|
||||
[\#4104](https://github.com/matrix-org/matrix-react-sdk/pull/4104)
|
||||
* Fix theme being overridden to light even after login is completed
|
||||
[\#4105](https://github.com/matrix-org/matrix-react-sdk/pull/4105)
|
||||
* Fix bug where SSSS could be overwritten if user never cross-signs
|
||||
[\#4100](https://github.com/matrix-org/matrix-react-sdk/pull/4100)
|
||||
* Accept canonical alias for pills
|
||||
[\#4096](https://github.com/matrix-org/matrix-react-sdk/pull/4096)
|
||||
* Fix: don't advertise ability to scan a QR code for verification
|
||||
[\#4094](https://github.com/matrix-org/matrix-react-sdk/pull/4094)
|
||||
* Fixes for printing event indexing stats.
|
||||
[\#4082](https://github.com/matrix-org/matrix-react-sdk/pull/4082)
|
||||
* Remove exec so release script continues
|
||||
[\#4095](https://github.com/matrix-org/matrix-react-sdk/pull/4095)
|
||||
* Use Persistent Storage where possible
|
||||
[\#4092](https://github.com/matrix-org/matrix-react-sdk/pull/4092)
|
||||
* Fix user page (missing null check)
|
||||
[\#4088](https://github.com/matrix-org/matrix-react-sdk/pull/4088)
|
||||
* Cancel verification request on dialog close
|
||||
[\#4081](https://github.com/matrix-org/matrix-react-sdk/pull/4081)
|
||||
* Fix various memory leaks due to method re-binding
|
||||
[\#4093](https://github.com/matrix-org/matrix-react-sdk/pull/4093)
|
||||
* Fix share message context menu option keyboard a11y
|
||||
[\#4073](https://github.com/matrix-org/matrix-react-sdk/pull/4073)
|
||||
|
||||
Changes in [2.1.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.1) (2020-02-19)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0...v2.1.1)
|
||||
|
@ -34,7 +34,7 @@ All code lands on the `develop` branch - `master` is only used for stable releas
|
||||
**Please file PRs against `develop`!!**
|
||||
|
||||
Please follow the standard Matrix contributor's guide:
|
||||
https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst
|
||||
https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst
|
||||
|
||||
Please follow the Matrix JS/React code style as per:
|
||||
https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md
|
||||
|
28
docs/scrolling.md
Normal file
28
docs/scrolling.md
Normal file
@ -0,0 +1,28 @@
|
||||
# ScrollPanel
|
||||
|
||||
## Updates
|
||||
|
||||
During an onscroll event, we check whether we're getting close to the top or bottom edge of the loaded content. If close enough, we fire a request to load more through the callback passed in the `onFillRequest` prop. This returns a promise is passed down from `TimelinePanel`, where it will call paginate on the `TimelineWindow` and once the events are received back, update its state with the new events. This update trickles down to the `MessagePanel`, which rerenders all tiles and passed that to `ScrollPanel`. ScrollPanels `componentDidUpdate` method gets called, and we do the scroll housekeeping there (read below). Once the rerender has completed, the `setState` callback is called and we resolve the promise returned by `onFillRequest`. Now we check the DOM to see if we need more fill requests.
|
||||
|
||||
## Prevent Shrinking
|
||||
|
||||
ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline.
|
||||
|
||||
|
||||
## BACAT (Bottom-Aligned, Clipped-At-Top) scrolling
|
||||
|
||||
BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842.
|
||||
|
||||
The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/riot-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles.
|
||||
|
||||
The approach taken instead is to vertically align the timeline tiles to the bottom of the scroll container (using flexbox) and give the timeline inside the scroll container an explicit height, initially set to a multiple of the PAGE_SIZE (400px at time of writing) as needed by the content. When scrolled up, we can compensate for anything that grew below the viewport by changing the height of the timeline to maintain what's currently visible in the viewport without adjusting the scrollTop and hence without jumping.
|
||||
|
||||
For anything above the viewport growing or shrinking, we don't need to do anything as the timeline is bottom-aligned. We do need to update the height manually to keep all content visible as more is loaded. To maintain scroll position after the portion above the viewport changes height, we need to set the scrollTop, as we cannot balance it out with more height changes. We do this 100ms after the user has stopped scrolling, so setting scrollTop has not nasty side-effects.
|
||||
|
||||
As of https://github.com/matrix-org/matrix-react-sdk/pull/4166, we are scrolling to compensate for height changes by calling `scrollBy(0, x)` rather than reading and than setting `scrollTop`, as reading `scrollTop` can (again, especially on macOS) easily return values that are out of sync with what is on the screen, probably because scrolling can be done [off the main thread](https://wiki.mozilla.org/Platform/GFX/APZ) in some circumstances. This seems to further prevent jumps.
|
||||
|
||||
### How does it work?
|
||||
|
||||
`componentDidUpdate` is called when a tile in the timeline is updated (as we rerender the whole timeline) or tiles are added or removed (see Updates section before). From here, `checkScroll` is called, which calls `_restoreSavedScrollState`. Now, we increase the timeline height if something below the viewport grew by adjusting `this._bottomGrowth`. `bottomGrowth` is the height added to the timeline (on top of the height from the number of pages calculated at the last `_updateHeight` run) to compensate for growth below the viewport. This is cleared during the next run of `_updateHeight`. Remember that the tiles in the timeline are aligned to the bottom.
|
||||
|
||||
From `_restoreSavedScrollState` we also call `_updateHeight` which waits until the user stops scrolling for 100ms and then recalculates the amount of pages of 400px the timeline should be sized to, to be able to show all of its (newly added) content. We have to adjust the scroll offset (which is why we wait until scrolling has stopped) now because the space above the viewport has likely changed.
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "2.1.1",
|
||||
"version": "2.2.3",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
@ -40,15 +40,15 @@
|
||||
"rethemendex": "res/css/rethemendex.sh",
|
||||
"clean": "rimraf lib",
|
||||
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:types": "tsc --emitDeclarationOnly",
|
||||
"build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js,.tsx\" src",
|
||||
"build:types": "tsc --emitDeclarationOnly --jsx react",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all",
|
||||
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
|
||||
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style",
|
||||
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
|
||||
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"lint:style": "stylelint 'res/css/**/*.scss'",
|
||||
"test": "jest",
|
||||
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
||||
@ -85,6 +85,7 @@
|
||||
"pako": "^1.0.5",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"prop-types": "^15.5.8",
|
||||
"qrcode": "^1.4.4",
|
||||
"qrcode-react": "^0.1.16",
|
||||
"qs": "^6.6.0",
|
||||
"react": "^16.9.0",
|
||||
@ -117,6 +118,7 @@
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@peculiar/webcrypto": "^1.0.22",
|
||||
"@types/react": "16.9",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"chokidar": "^3.3.1",
|
||||
|
@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-react-sdk.
|
||||
#
|
||||
@ -11,6 +11,7 @@ cd `dirname $0`
|
||||
|
||||
for i in matrix-js-sdk
|
||||
do
|
||||
echo "Checking version of $i..."
|
||||
depver=`cat package.json | jq -r .dependencies[\"$i\"]`
|
||||
latestver=`yarn info -s $i dist-tags.next`
|
||||
if [ "$depver" != "$latestver" ]
|
||||
|
@ -42,10 +42,15 @@ pre, code {
|
||||
font-size: 100% !important;
|
||||
}
|
||||
|
||||
.error, .warning {
|
||||
.error, .warning,
|
||||
.text-error, .text-warning {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: $accent-color;
|
||||
}
|
||||
|
||||
b {
|
||||
// On Firefox, the default weight for `<b>` is `bolder` which results in no bold
|
||||
// effect since we only have specific weights of our fonts available.
|
||||
|
@ -37,6 +37,10 @@ limitations under the License.
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_waiting {
|
||||
color: $notice-secondary-color;
|
||||
}
|
||||
|
||||
.mx_CompleteSecurity_actionRow {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
@ -189,3 +189,37 @@ limitations under the License.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_DevTools_VerificationRequest {
|
||||
border: 1px solid #cccccc;
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
margin-bottom: 6px;
|
||||
font-family: $monospace-font-family;
|
||||
|
||||
dl {
|
||||
display: grid;
|
||||
grid-template-columns: max-content auto;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dd {
|
||||
grid-column-start: 2;
|
||||
}
|
||||
|
||||
dd:empty {
|
||||
color: #666666;
|
||||
&::after {
|
||||
content: "(empty)";
|
||||
}
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: bold;
|
||||
grid-column-start: 1;
|
||||
}
|
||||
|
||||
dt::after {
|
||||
content: ":";
|
||||
}
|
||||
}
|
||||
|
@ -20,14 +20,21 @@ limitations under the License.
|
||||
}
|
||||
|
||||
.mx_EditableItem {
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.mx_EditableItem_delete {
|
||||
order: 3;
|
||||
margin-right: 5px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
mask-image: url('$(res)/img/feather-customised/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
background-color: $warning-color;
|
||||
mask-size: 100%;
|
||||
}
|
||||
|
||||
.mx_EditableItem_email {
|
||||
@ -36,12 +43,19 @@ limitations under the License.
|
||||
|
||||
.mx_EditableItem_promptText {
|
||||
margin-right: 10px;
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.mx_EditableItem_confirmBtn {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mx_EditableItem_item {
|
||||
flex: auto 1 0;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.mx_EditableItemList_label {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,11 @@
|
||||
padding-left: 5px;
|
||||
}
|
||||
|
||||
a.mx_Pill {
|
||||
word-break: break-all;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
/* More specific to override `.markdown-body a` text-decoration */
|
||||
.mx_EventTile_content .markdown-body a.mx_Pill {
|
||||
text-decoration: none;
|
||||
|
@ -137,12 +137,19 @@ limitations under the License.
|
||||
font-size: 18px;
|
||||
line-height: 25px;
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
max-height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
// limit to 2 lines, show an ellipsis if it overflows
|
||||
// this looks webkit specific but is supported by Firefox 68+
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.mx_E2EIcon {
|
||||
margin: 5px;
|
||||
}
|
||||
|
@ -26,3 +26,21 @@ limitations under the License.
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.mx_AliasSettings {
|
||||
summary {
|
||||
cursor: pointer;
|
||||
color: $accent-color;
|
||||
font-weight: 600;
|
||||
list-style: none;
|
||||
|
||||
// list-style doesn't do it for webkit
|
||||
&::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AliasSettings_localAliasHeader {
|
||||
margin-top: 35px;
|
||||
}
|
||||
}
|
||||
|
@ -14,27 +14,45 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_InviteOnlyIcon {
|
||||
@define-mixin mx_InviteOnlyIcon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
position: relative;
|
||||
display: block !important;
|
||||
// Align the padlock with unencrypted room names
|
||||
}
|
||||
|
||||
@define-mixin mx_InviteOnlyIcon_padlock {
|
||||
background-color: $roomtile-name-color;
|
||||
mask-image: url("$(res)/img/feather-customised/lock-solid.svg");
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mx_InviteOnlyIcon_large {
|
||||
@mixin mx_InviteOnlyIcon;
|
||||
margin: 0 4px;
|
||||
|
||||
&::before {
|
||||
background-color: $roomtile-name-color;
|
||||
mask-image: url('$(res)/img/feather-customised/lock-solid.svg');
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
@mixin mx_InviteOnlyIcon_padlock;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteOnlyIcon_small {
|
||||
@mixin mx_InviteOnlyIcon;
|
||||
left: -2px;
|
||||
|
||||
&::before {
|
||||
@mixin mx_InviteOnlyIcon_padlock;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ limitations under the License.
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
// break-word, with fallback to break-all, which is wider supported
|
||||
word-break: break-all;
|
||||
word-break: break-word;
|
||||
|
||||
&.mx_RoomPreviewBar_spinnerTitle {
|
||||
display: flex;
|
||||
|
@ -51,8 +51,30 @@ limitations under the License.
|
||||
position: absolute;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
mask: url('$(res)/img/icon-jump-to-first-unread.svg');
|
||||
mask-image: url('$(res)/img/icon-jump-to-first-unread.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: 9px 13px;
|
||||
background: $roomtile-name-color;
|
||||
}
|
||||
|
||||
.mx_TopUnreadMessagesBar_markAsRead {
|
||||
display: block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background: $primary-bg-color;
|
||||
border: 1.3px solid $roomtile-name-color;
|
||||
border-radius: 10px;
|
||||
margin: 5px auto;
|
||||
}
|
||||
|
||||
.mx_TopUnreadMessagesBar_markAsRead::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
mask-image: url('$(res)/img/cancel.svg');
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 10px;
|
||||
mask-position: 4px 4px;
|
||||
background: $roomtile-name-color;
|
||||
}
|
||||
|
@ -45,9 +45,17 @@ limitations under the License.
|
||||
margin: 10px 100px 10px 0; // Align with the rest of the view
|
||||
}
|
||||
|
||||
.mx_SettingsTab_section .mx_SettingsFlag {
|
||||
margin-right: 100px;
|
||||
margin-bottom: 10px;
|
||||
.mx_SettingsTab_section {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.mx_SettingsFlag {
|
||||
margin-right: 100px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&.mx_SettingsTab_subsectionText .mx_SettingsFlag {
|
||||
margin-right: 0px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label {
|
||||
|
@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_PreferencesUserSettingsTab .mx_Field {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
.mx_PreferencesUserSettingsTab {
|
||||
.mx_Field {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
}
|
||||
|
||||
.mx_SettingsTab_section {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
}
|
||||
|
@ -5,9 +5,12 @@
|
||||
Arial empirically gets it right, hence prioritising Arial here. */
|
||||
/* We fall through to Twemoji for emoji rather than falling through
|
||||
to native Emoji fonts (if any) to ensure cross-browser consistency */
|
||||
$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif;
|
||||
/* Noto Color Emoji contains digits, in fixed-width, therefore causing
|
||||
digits in flowed text to stand out.
|
||||
TODO: Consider putting all emoji fonts to the end rather than the front. */
|
||||
$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji';
|
||||
|
||||
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace;
|
||||
$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji';
|
||||
|
||||
// unified palette
|
||||
// try to use these colors when possible
|
||||
|
@ -237,7 +237,7 @@ const walkOpts = {
|
||||
const fullPath = path.join(root, fileStats.name);
|
||||
|
||||
let trs;
|
||||
if (fileStats.name.endsWith('.js')) {
|
||||
if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.tsx')) {
|
||||
trs = getTranslationsJs(fullPath);
|
||||
} else if (fileStats.name.endsWith('.html')) {
|
||||
trs = getTranslationsOther(fullPath);
|
||||
|
@ -14,8 +14,10 @@ echo "generating $out"
|
||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
EOF
|
||||
|
||||
./node_modules/.bin/eslint --no-ignore -f json src test |
|
||||
|
||||
./node_modules/.bin/eslint -f json src test |
|
||||
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
|
||||
sed -e 's/.*matrix-react-sdk\///';
|
||||
} > "$out"
|
||||
# also append rules from eslintignore file
|
||||
cat .eslintignore >> $out
|
||||
|
@ -8,11 +8,14 @@ var chokidar = require('chokidar');
|
||||
var componentIndex = path.join('src', 'component-index.js');
|
||||
var componentIndexTmp = componentIndex+".tmp";
|
||||
var componentsDir = path.join('src', 'components');
|
||||
var componentGlob = '**/*.js';
|
||||
var componentJsGlob = '**/*.js';
|
||||
var componentTsGlob = '**/*.tsx';
|
||||
var prevFiles = [];
|
||||
|
||||
function reskindex() {
|
||||
var files = glob.sync(componentGlob, {cwd: componentsDir}).sort();
|
||||
var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
|
||||
var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
|
||||
var files = [...tsFiles, ...jsFiles];
|
||||
if (!filesHaveChanged(files, prevFiles)) {
|
||||
return;
|
||||
}
|
||||
@ -36,7 +39,7 @@ function reskindex() {
|
||||
strm.write("let components = {};\n");
|
||||
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
var file = files[i].replace('.js', '');
|
||||
var file = files[i].replace('.js', '').replace('.tsx', '');
|
||||
|
||||
var moduleName = (file.replace(/\//g, '.'));
|
||||
var importName = moduleName.replace(/\./g, "$");
|
||||
@ -79,7 +82,7 @@ if (!args.w) {
|
||||
}
|
||||
|
||||
var watchDebouncer = null;
|
||||
chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => {
|
||||
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
|
||||
if (path === componentIndex) return;
|
||||
if (watchDebouncer) clearTimeout(watchDebouncer);
|
||||
watchDebouncer = setTimeout(reskindex, 1000);
|
||||
|
@ -260,7 +260,6 @@ class Analytics {
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Analytics error: ", e);
|
||||
window.err = e;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,6 +102,8 @@ export function getInitialLetter(name) {
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
width,
|
||||
|
@ -4,6 +4,7 @@
|
||||
Copyright 2016 Aviral Dasgupta
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 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.
|
||||
@ -18,6 +19,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {MatrixClient} from "matrix-js-sdk";
|
||||
import dis from './dispatcher';
|
||||
import BaseEventIndexManager from './indexing/BaseEventIndexManager';
|
||||
|
||||
@ -162,4 +164,28 @@ export default class BasePlatform {
|
||||
getEventIndexingManager(): BaseEventIndexManager | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
setLanguage(preferredLangs: string[]) {}
|
||||
|
||||
getSSOCallbackUrl(hsUrl: string, isUrl: string): URL {
|
||||
const url = new URL(window.location.href);
|
||||
// XXX: at this point, the fragment will always be #/login, which is no
|
||||
// use to anyone. Ideally, we would get the intended fragment from
|
||||
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
|
||||
// through an SSO login.
|
||||
url.hash = "";
|
||||
url.searchParams.set("homeserver", hsUrl);
|
||||
url.searchParams.set("identityServer", isUrl);
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Single Sign On flows.
|
||||
* @param {MatrixClient} mxClient the matrix client using which we should start the flow
|
||||
* @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO.
|
||||
*/
|
||||
startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") {
|
||||
const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl());
|
||||
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ function _setCallListeners(call) {
|
||||
"if you proceed without verifying them, it will be "+
|
||||
"possible for someone to eavesdrop on your call.",
|
||||
),
|
||||
button: _t('Review Devices'),
|
||||
button: _t('Review Sessions'),
|
||||
onFinished: function(confirmed) {
|
||||
if (confirmed) {
|
||||
const room = MatrixClientPeg.get().getRoom(call.roomId);
|
||||
|
@ -21,6 +21,7 @@ import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||
import { _t } from './languageHandler';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
@ -125,8 +126,37 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||
return [name, key];
|
||||
}
|
||||
|
||||
const onSecretRequested = async function({
|
||||
user_id: userId,
|
||||
device_id: deviceId,
|
||||
request_id: requestId,
|
||||
name,
|
||||
device_trust: deviceTrust,
|
||||
}) {
|
||||
console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust);
|
||||
const client = MatrixClientPeg.get();
|
||||
if (userId !== client.getUserId()) {
|
||||
return;
|
||||
}
|
||||
if (!deviceTrust || !deviceTrust.isVerified()) {
|
||||
console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`);
|
||||
return;
|
||||
}
|
||||
const callbacks = client.getCrossSigningCacheCallbacks();
|
||||
if (!callbacks.getCrossSigningKeyCache) return;
|
||||
if (name === "m.cross_signing.self_signing") {
|
||||
const key = await callbacks.getCrossSigningKeyCache("self_signing");
|
||||
return key && encodeBase64(key);
|
||||
} else if (name === "m.cross_signing.user_signing") {
|
||||
const key = await callbacks.getCrossSigningKeyCache("user_signing");
|
||||
return key && encodeBase64(key);
|
||||
}
|
||||
console.warn("onSecretRequested didn't recognise the secret named ", name);
|
||||
};
|
||||
|
||||
export const crossSigningCallbacks = {
|
||||
getSecretStorageKey,
|
||||
onSecretRequested,
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -99,9 +99,13 @@ export default class DeviceListener {
|
||||
}
|
||||
|
||||
async _recheck() {
|
||||
if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return;
|
||||
const cli = MatrixClientPeg.get();
|
||||
|
||||
if (
|
||||
!SettingsStore.isFeatureEnabled("feature_cross_signing") ||
|
||||
!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) return;
|
||||
|
||||
if (!cli.isCryptoEnabled()) return;
|
||||
if (!cli.getCrossSigningId()) {
|
||||
if (this._dismissedThisDeviceToast) {
|
||||
|
@ -23,7 +23,6 @@ import ReplyThread from "./components/views/elements/ReplyThread";
|
||||
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import highlight from 'highlight.js';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import _linkifyElement from 'linkifyjs/element';
|
||||
@ -160,7 +159,7 @@ const transformTags = { // custom to matrix
|
||||
delete attribs.target;
|
||||
}
|
||||
}
|
||||
attribs.rel = 'noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
attribs.rel = 'noreferrer noopener'; // https://mathiasbynens.github.io/rel-noopener/
|
||||
return { tagName, attribs };
|
||||
},
|
||||
'img': function(tagName, attribs) {
|
||||
@ -467,11 +466,12 @@ export function bodyToHtml(content, highlights, opts={}) {
|
||||
/**
|
||||
* Linkifies the given string. This is a wrapper around 'linkifyjs/string'.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
* @param {string} str string to linkify
|
||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||
* @returns {string} Linkified string
|
||||
*/
|
||||
export function linkifyString(str) {
|
||||
return _linkifyString(str);
|
||||
export function linkifyString(str, options = linkifyMatrix.options) {
|
||||
return _linkifyString(str, options);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -489,10 +489,11 @@ export function linkifyElement(element, options = linkifyMatrix.options) {
|
||||
* Linkify the given string and sanitize the HTML afterwards.
|
||||
*
|
||||
* @param {string} dirtyHtml The HTML string to sanitize and linkify
|
||||
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
|
||||
* @returns {string}
|
||||
*/
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml) {
|
||||
return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams);
|
||||
export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) {
|
||||
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -181,24 +181,12 @@ export default class IdentityAuthClient {
|
||||
}
|
||||
|
||||
async registerForToken(check=true) {
|
||||
try {
|
||||
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||
const { access_token: accessToken, token } =
|
||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
const identityAccessToken = token ? token : accessToken;
|
||||
if (check) await this._checkToken(identityAccessToken);
|
||||
return identityAccessToken;
|
||||
} catch (e) {
|
||||
if (e.cors === "rejected" || e.httpStatus === 404) {
|
||||
// Assume IS only supports deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
console.warn("IS doesn't support v2 auth");
|
||||
this.authEnabled = false;
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken();
|
||||
// XXX: The spec is `token`, but we used `access_token` for a Sydent release.
|
||||
const { access_token: accessToken, token } =
|
||||
await this._matrixClient.registerWithIdentityServer(hsOpenIdToken);
|
||||
const identityAccessToken = token ? token : accessToken;
|
||||
if (check) await this._checkToken(identityAccessToken);
|
||||
return identityAccessToken;
|
||||
}
|
||||
}
|
||||
|
28
src/Login.js
28
src/Login.js
@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
|
||||
Copyright 2020 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.
|
||||
@ -19,8 +20,6 @@ limitations under the License.
|
||||
|
||||
import Matrix from "matrix-js-sdk";
|
||||
|
||||
import url from 'url';
|
||||
|
||||
export default class Login {
|
||||
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
||||
this._hsUrl = hsUrl;
|
||||
@ -29,6 +28,7 @@ export default class Login {
|
||||
this._currentFlowIndex = 0;
|
||||
this._flows = [];
|
||||
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this._tempClient = null; // memoize
|
||||
}
|
||||
|
||||
getHomeserverUrl() {
|
||||
@ -40,10 +40,12 @@ export default class Login {
|
||||
}
|
||||
|
||||
setHomeserverUrl(hsUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._hsUrl = hsUrl;
|
||||
}
|
||||
|
||||
setIdentityServerUrl(isUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._isUrl = isUrl;
|
||||
}
|
||||
|
||||
@ -52,8 +54,9 @@ export default class Login {
|
||||
* requests.
|
||||
* @returns {MatrixClient}
|
||||
*/
|
||||
_createTemporaryClient() {
|
||||
return Matrix.createClient({
|
||||
createTemporaryClient() {
|
||||
if (this._tempClient) return this._tempClient; // use memoization
|
||||
return this._tempClient = Matrix.createClient({
|
||||
baseUrl: this._hsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
});
|
||||
@ -61,7 +64,7 @@ export default class Login {
|
||||
|
||||
getFlows() {
|
||||
const self = this;
|
||||
const client = this._createTemporaryClient();
|
||||
const client = this.createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._currentFlowIndex = 0;
|
||||
@ -139,21 +142,6 @@ export default class Login {
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
getSsoLoginUrl(loginType) {
|
||||
const client = this._createTemporaryClient();
|
||||
const parsedUrl = url.parse(window.location.href, true);
|
||||
|
||||
// XXX: at this point, the fragment will always be #/login, which is no
|
||||
// use to anyone. Ideally, we would get the intended fragment from
|
||||
// MatrixChat.screenAfterLogin so that you could follow #/room links etc
|
||||
// through an SSO login.
|
||||
parsedUrl.hash = "";
|
||||
|
||||
parsedUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
parsedUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
return client.getSsoLoginUrl(url.format(parsedUrl), loginType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -136,7 +136,7 @@ export default class Markdown {
|
||||
// thus opening in a new tab.
|
||||
if (externalLinks) {
|
||||
attrs.push(['target', '_blank']);
|
||||
attrs.push(['rel', 'noopener']);
|
||||
attrs.push(['rel', 'noreferrer noopener']);
|
||||
}
|
||||
this.tag('a', attrs);
|
||||
} else {
|
||||
|
@ -39,6 +39,8 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/;
|
||||
* If true, goes to the home page if the user cancels the action
|
||||
* @param {bool} options.go_welcome_on_cancel
|
||||
* If true, goes to the welcome page if the user cancels the action
|
||||
* @param {bool} options.screen_after
|
||||
* If present the screen to redirect to after a successful login or register.
|
||||
*/
|
||||
export async function startAnyRegistrationFlow(options) {
|
||||
if (options === undefined) options = {};
|
||||
@ -66,13 +68,21 @@ export async function startAnyRegistrationFlow(options) {
|
||||
// });
|
||||
//} else {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
title: _t("Registration Required"),
|
||||
description: _t("You need to register to do this. Would you like to register now?"),
|
||||
button: _t("Register"),
|
||||
const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
quitOnly: true,
|
||||
title: _t("Sign In or Create Account"),
|
||||
description: _t("Use your account or create a new one to continue."),
|
||||
button: _t("Create Account"),
|
||||
extraButtons: [
|
||||
<button key="start_login" onClick={() => {
|
||||
modal.close();
|
||||
dis.dispatch({action: 'start_login', screenAfterLogin: options.screen_after});
|
||||
}}>{ _t('Sign In') }</button>,
|
||||
],
|
||||
onFinished: (proceed) => {
|
||||
if (proceed) {
|
||||
dis.dispatch({action: 'start_registration'});
|
||||
dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after});
|
||||
} else if (options.go_home_on_cancel) {
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
} else if (options.go_welcome_on_cancel) {
|
||||
@ -101,4 +111,3 @@ export async function startAnyRegistrationFlow(options) {
|
||||
// }
|
||||
// throw new Error("Register request succeeded when it should have returned 401!");
|
||||
// }
|
||||
|
||||
|
@ -23,7 +23,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
* of aliases. Otherwise return null;
|
||||
*/
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.getCanonicalAlias() || room.getAliases()[0];
|
||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -893,6 +893,26 @@ export const CommandMap = {
|
||||
},
|
||||
category: CommandCategories.advanced,
|
||||
}),
|
||||
|
||||
whois: new Command({
|
||||
name: "whois",
|
||||
description: _td("Displays information about a user"),
|
||||
args: '<user-id>',
|
||||
runFn: function(roomId, userId) {
|
||||
if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
|
||||
return reject(this.getUsage());
|
||||
}
|
||||
|
||||
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'view_user',
|
||||
member: member || {userId},
|
||||
});
|
||||
return success();
|
||||
},
|
||||
category: CommandCategories.advanced,
|
||||
}),
|
||||
};
|
||||
/* eslint-enable babel/no-invalid-this */
|
||||
|
||||
|
@ -127,6 +127,13 @@ function textForRoomNameEvent(ev) {
|
||||
if (!ev.getContent().name || ev.getContent().name.trim().length === 0) {
|
||||
return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName});
|
||||
}
|
||||
if (ev.getPrevContent().name) {
|
||||
return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', {
|
||||
senderDisplayName,
|
||||
oldRoomName: ev.getPrevContent().name,
|
||||
newRoomName: ev.getContent().name,
|
||||
});
|
||||
}
|
||||
return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', {
|
||||
senderDisplayName,
|
||||
roomName: ev.getContent().name,
|
||||
@ -269,85 +276,55 @@ function textForMessageEvent(ev) {
|
||||
return message;
|
||||
}
|
||||
|
||||
function textForRoomAliasesEvent(ev) {
|
||||
// An alternative implementation of this as a first-class event can be found at
|
||||
// https://github.com/matrix-org/matrix-react-sdk/blob/dc7212ec2bd12e1917233ed7153b3e0ef529a135/src/components/views/messages/RoomAliasesEvent.js
|
||||
// This feels a bit overkill though, and it's not clear the i18n really needs it
|
||||
// so instead it's landing as a simple textual event.
|
||||
|
||||
const maxShown = 3;
|
||||
|
||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const oldAliases = ev.getPrevContent().aliases || [];
|
||||
const newAliases = ev.getContent().aliases || [];
|
||||
|
||||
const addedAliases = newAliases.filter((x) => !oldAliases.includes(x));
|
||||
const removedAliases = oldAliases.filter((x) => !newAliases.includes(x));
|
||||
|
||||
if (!addedAliases.length && !removedAliases.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (addedAliases.length && !removedAliases.length) {
|
||||
if (addedAliases.length > maxShown) {
|
||||
return _t("%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room", {
|
||||
senderName: senderName,
|
||||
count: addedAliases.length - maxShown,
|
||||
addedAddresses: addedAliases.slice(0, maxShown).join(', '),
|
||||
});
|
||||
}
|
||||
return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
count: addedAliases.length,
|
||||
addedAddresses: addedAliases.join(', '),
|
||||
});
|
||||
} else if (!addedAliases.length && removedAliases.length) {
|
||||
if (removedAliases.length > maxShown) {
|
||||
return _t("%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room", {
|
||||
senderName: senderName,
|
||||
count: removedAliases.length - maxShown,
|
||||
removedAddresses: removedAliases.slice(0, maxShown).join(', '),
|
||||
});
|
||||
}
|
||||
return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
count: removedAliases.length,
|
||||
removedAddresses: removedAliases.join(', '),
|
||||
});
|
||||
} else {
|
||||
const combined = addedAliases.length + removedAliases.length;
|
||||
if (combined > maxShown) {
|
||||
return _t("%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room", {
|
||||
senderName: senderName,
|
||||
countAdded: addedAliases.length,
|
||||
countRemoved: removedAliases.length,
|
||||
});
|
||||
}
|
||||
return _t(
|
||||
'%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', {
|
||||
senderName: senderName,
|
||||
addedAddresses: addedAliases.join(', '),
|
||||
removedAddresses: removedAliases.join(', '),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function textForCanonicalAliasEvent(ev) {
|
||||
const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const oldAlias = ev.getPrevContent().alias;
|
||||
const oldAltAliases = ev.getPrevContent().alt_aliases || [];
|
||||
const newAlias = ev.getContent().alias;
|
||||
const newAltAliases = ev.getContent().alt_aliases || [];
|
||||
const removedAltAliases = oldAltAliases.filter(alias => !newAltAliases.includes(alias));
|
||||
const addedAltAliases = newAltAliases.filter(alias => !oldAltAliases.includes(alias));
|
||||
|
||||
if (newAlias) {
|
||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||
senderName: senderName,
|
||||
address: ev.getContent().alias,
|
||||
});
|
||||
} else if (oldAlias) {
|
||||
return _t('%(senderName)s removed the main address for this room.', {
|
||||
if (!removedAltAliases.length && !addedAltAliases.length) {
|
||||
if (newAlias) {
|
||||
return _t('%(senderName)s set the main address for this room to %(address)s.', {
|
||||
senderName: senderName,
|
||||
address: ev.getContent().alias,
|
||||
});
|
||||
} else if (oldAlias) {
|
||||
return _t('%(senderName)s removed the main address for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
} else if (newAlias === oldAlias) {
|
||||
if (addedAltAliases.length && !removedAltAliases.length) {
|
||||
return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', {
|
||||
senderName: senderName,
|
||||
addresses: addedAltAliases.join(", "),
|
||||
count: addedAltAliases.length,
|
||||
});
|
||||
} if (removedAltAliases.length && !addedAltAliases.length) {
|
||||
return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', {
|
||||
senderName: senderName,
|
||||
addresses: removedAltAliases.join(", "),
|
||||
count: removedAltAliases.length,
|
||||
});
|
||||
} if (removedAltAliases.length && addedAltAliases.length) {
|
||||
return _t('%(senderName)s changed the alternative addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// both alias and alt_aliases where modified
|
||||
return _t('%(senderName)s changed the main and alternative addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
// in case there is no difference between the two events,
|
||||
// say something as we can't simply hide the tile from here
|
||||
return _t('%(senderName)s changed the addresses for this room.', {
|
||||
senderName: senderName,
|
||||
});
|
||||
}
|
||||
|
||||
function textForCallAnswerEvent(event) {
|
||||
@ -612,7 +589,6 @@ const handlers = {
|
||||
};
|
||||
|
||||
const stateHandlers = {
|
||||
'm.room.aliases': textForRoomAliasesEvent,
|
||||
'm.room.canonical_alias': textForCanonicalAliasEvent,
|
||||
'm.room.name': textForRoomNameEvent,
|
||||
'm.room.topic': textForTopicEvent,
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { asyncAction } from './actionCreators';
|
||||
import RoomListStore from '../stores/RoomListStore';
|
||||
import RoomListStore, {TAG_DM} from '../stores/RoomListStore';
|
||||
import Modal from '../Modal';
|
||||
import * as Rooms from '../Rooms';
|
||||
import { _t } from '../languageHandler';
|
||||
@ -73,11 +73,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||
const roomId = room.roomId;
|
||||
|
||||
// Evil hack to get DMs behaving
|
||||
if ((oldTag === undefined && newTag === 'im.vector.fake.direct') ||
|
||||
(oldTag === 'im.vector.fake.direct' && newTag === undefined)
|
||||
if ((oldTag === undefined && newTag === TAG_DM) ||
|
||||
(oldTag === TAG_DM && newTag === undefined)
|
||||
) {
|
||||
return Rooms.guessAndSetDMRoom(
|
||||
room, newTag === 'im.vector.fake.direct',
|
||||
room, newTag === TAG_DM,
|
||||
).catch((err) => {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
console.error("Failed to set direct chat tag " + err);
|
||||
@ -91,10 +91,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||
const hasChangedSubLists = oldTag !== newTag;
|
||||
|
||||
// More evilness: We will still be dealing with moving to favourites/low prio,
|
||||
// but we avoid ever doing a request with 'im.vector.fake.direct`.
|
||||
// but we avoid ever doing a request with TAG_DM.
|
||||
//
|
||||
// if we moved lists, remove the old tag
|
||||
if (oldTag && oldTag !== 'im.vector.fake.direct' &&
|
||||
if (oldTag && oldTag !== TAG_DM &&
|
||||
hasChangedSubLists
|
||||
) {
|
||||
const promiseToDelete = matrixClient.deleteRoomTag(
|
||||
@ -112,7 +112,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex,
|
||||
}
|
||||
|
||||
// if we moved lists or the ordering changed, add the new tag
|
||||
if (newTag && newTag !== 'im.vector.fake.direct' &&
|
||||
if (newTag && newTag !== TAG_DM &&
|
||||
(hasChangedSubLists || metaData)
|
||||
) {
|
||||
// metaData is the body of the PUT to set the tag, so it must
|
||||
|
@ -141,15 +141,17 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||
let crawlerState;
|
||||
|
||||
if (this.state.currentRoom === null) {
|
||||
crawlerState = _t("Not currently downloading messages for any room.");
|
||||
crawlerState = _t("Not currently indexing messages for any room.");
|
||||
} else {
|
||||
crawlerState = (
|
||||
_t("Downloading mesages for %(currentRoom)s.", { currentRoom: this.state.currentRoom })
|
||||
_t("Currently indexing: %(currentRoom)s.", { currentRoom: this.state.currentRoom })
|
||||
);
|
||||
}
|
||||
|
||||
const Field = sdk.getComponent('views.elements.Field');
|
||||
|
||||
const doneRooms = Math.max(0, (this.state.roomCount - this.state.crawlingRoomsCount));
|
||||
|
||||
const eventIndexingSettings = (
|
||||
<div>
|
||||
{
|
||||
@ -158,13 +160,13 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||
)
|
||||
}
|
||||
<div className='mx_SettingsTab_subsectionText'>
|
||||
{crawlerState}<br />
|
||||
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
||||
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
||||
{_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", {
|
||||
crawlingRooms: formatCountLong(this.state.crawlingRoomsCount),
|
||||
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", {
|
||||
doneRooms: formatCountLong(doneRooms),
|
||||
totalRooms: formatCountLong(this.state.roomCount),
|
||||
})} <br />
|
||||
{crawlerState}<br />
|
||||
<Field
|
||||
id={"crawlerSleepTimeMs"}
|
||||
label={_t('Message downloading sleep time(ms)')}
|
||||
|
@ -53,7 +53,6 @@ function selectText(target) {
|
||||
*/
|
||||
export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
static propTypes = {
|
||||
secureSecretStorage: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
@ -65,7 +64,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
this._setZxcvbnResultTimeout = null;
|
||||
|
||||
this.state = {
|
||||
secureSecretStorage: props.secureSecretStorage,
|
||||
secureSecretStorage: null,
|
||||
phase: PHASE_PASSPHRASE,
|
||||
passPhrase: '',
|
||||
passPhraseConfirm: '',
|
||||
@ -73,23 +72,20 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||
downloaded: false,
|
||||
zxcvbnResult: null,
|
||||
};
|
||||
|
||||
if (this.state.secureSecretStorage === undefined) {
|
||||
this.state.secureSecretStorage =
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
}
|
||||
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (this.state.secureSecretStorage) {
|
||||
this.state.phase = PHASE_BACKINGUP;
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
async componentDidMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const secureSecretStorage = (
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
);
|
||||
this.setState({ secureSecretStorage });
|
||||
|
||||
// If we're using secret storage, skip ahead to the backing up step, as
|
||||
// `accessSecretStorage` will handle passphrases as needed.
|
||||
if (this.state.secureSecretStorage) {
|
||||
if (secureSecretStorage) {
|
||||
this.setState({ phase: PHASE_BACKINGUP });
|
||||
this._createBackup();
|
||||
}
|
||||
}
|
||||
|
@ -83,7 +83,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
// does the server offer a UI auth flow with just m.login.password
|
||||
// for /keys/device_signing/upload?
|
||||
canUploadKeysWithPasswordOnly: null,
|
||||
accountPassword: props.accountPassword,
|
||||
accountPassword: props.accountPassword || "",
|
||||
accountPasswordCorrect: null,
|
||||
// status of the key backup toggle switch
|
||||
useKeyBackup: true,
|
||||
@ -117,6 +117,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
});
|
||||
|
||||
return {
|
||||
backupInfo,
|
||||
backupSigStatus,
|
||||
};
|
||||
}
|
||||
|
||||
async _queryKeyUploadAuth() {
|
||||
@ -269,13 +274,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog');
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null,
|
||||
/* priority = */ false, /* static = */ true,
|
||||
/* priority = */ false, /* static = */ false,
|
||||
);
|
||||
|
||||
await finished;
|
||||
await this._fetchBackupInfo();
|
||||
const { backupSigStatus } = await this._fetchBackupInfo();
|
||||
if (
|
||||
this.state.backupSigStatus.usable &&
|
||||
backupSigStatus.usable &&
|
||||
this.state.canUploadKeysWithPasswordOnly &&
|
||||
this.state.accountPassword
|
||||
) {
|
||||
@ -400,12 +405,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
|
||||
let authPrompt;
|
||||
let nextCaption = _t("Next");
|
||||
if (!this.state.backupSigStatus.usable) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
||||
</div>;
|
||||
nextCaption = _t("Restore");
|
||||
} else if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
if (this.state.canUploadKeysWithPasswordOnly) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
||||
<div><Field
|
||||
@ -418,6 +418,11 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
autoFocus={true}
|
||||
/></div>
|
||||
</div>;
|
||||
} else if (!this.state.backupSigStatus.usable) {
|
||||
authPrompt = <div>
|
||||
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
|
||||
</div>;
|
||||
nextCaption = _t("Restore");
|
||||
} else {
|
||||
authPrompt = <p>
|
||||
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
|
||||
@ -433,6 +438,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||
<div>{authPrompt}</div>
|
||||
<DialogButtons
|
||||
primaryButton={nextCaption}
|
||||
onPrimaryButtonClick={this._onMigrateFormSubmit}
|
||||
hasCancel={false}
|
||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||
>
|
||||
|
@ -23,7 +23,6 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||
import QueryMatcher from './QueryMatcher';
|
||||
import {PillCompletion} from './Components';
|
||||
import {getDisplayAliasForRoom} from '../Rooms';
|
||||
import * as sdk from '../index';
|
||||
import _sortBy from 'lodash/sortBy';
|
||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
||||
@ -40,11 +39,19 @@ function score(query, space) {
|
||||
}
|
||||
}
|
||||
|
||||
function matcherObject(room, displayedAlias, matchName = "") {
|
||||
return {
|
||||
room,
|
||||
matchName,
|
||||
displayedAlias,
|
||||
};
|
||||
}
|
||||
|
||||
export default class RoomProvider extends AutocompleteProvider {
|
||||
constructor() {
|
||||
super(ROOM_REGEX);
|
||||
this.matcher = new QueryMatcher([], {
|
||||
keys: ['displayedAlias', 'name'],
|
||||
keys: ['displayedAlias', 'matchName'],
|
||||
});
|
||||
}
|
||||
|
||||
@ -56,16 +63,16 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||
if (command) {
|
||||
// the only reason we need to do this is because Fuse only matches on properties
|
||||
let matcherObjects = client.getVisibleRooms().filter(
|
||||
(room) => !!room && !!getDisplayAliasForRoom(room),
|
||||
).map((room) => {
|
||||
return {
|
||||
room: room,
|
||||
name: room.name,
|
||||
displayedAlias: getDisplayAliasForRoom(room),
|
||||
};
|
||||
});
|
||||
|
||||
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => {
|
||||
if (room.getCanonicalAlias()) {
|
||||
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
|
||||
}
|
||||
if (room.getAltAliases().length) {
|
||||
const altAliases = room.getAltAliases().map(alias => matcherObject(room, alias));
|
||||
aliases = aliases.concat(altAliases);
|
||||
}
|
||||
return aliases;
|
||||
}, []);
|
||||
// Filter out any matches where the user will have also autocompleted new rooms
|
||||
matcherObjects = matcherObjects.filter((r) => {
|
||||
const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||
@ -84,16 +91,16 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||
completions = _sortBy(completions, [
|
||||
(c) => score(matchedString, c.displayedAlias),
|
||||
(c) => c.displayedAlias.length,
|
||||
]).map((room) => {
|
||||
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
||||
]);
|
||||
completions = completions.map((room) => {
|
||||
return {
|
||||
completion: displayAlias,
|
||||
completionId: displayAlias,
|
||||
completion: room.displayedAlias,
|
||||
completionId: room.room.roomId,
|
||||
type: "room",
|
||||
suffix: ' ',
|
||||
href: makeRoomPermalink(displayAlias),
|
||||
href: makeRoomPermalink(room.displayedAlias),
|
||||
component: (
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.name} description={displayAlias} />
|
||||
<PillCompletion initialComponent={<RoomAvatar width={24} height={24} room={room.room} />} title={room.room.name} description={room.displayedAlias} />
|
||||
),
|
||||
range,
|
||||
};
|
||||
|
@ -255,7 +255,7 @@ export class ContextMenu extends React.Component {
|
||||
|
||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||
chevronOffset.left = props.chevronOffset;
|
||||
} else {
|
||||
} else if (position.top !== undefined) {
|
||||
const target = position.top;
|
||||
|
||||
// By default, no adjustment is made
|
||||
|
@ -481,7 +481,7 @@ export default createReactClass({
|
||||
group_id: groupId,
|
||||
},
|
||||
});
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}});
|
||||
willDoOnboarding = true;
|
||||
}
|
||||
if (stateKey === GroupStore.STATE_KEY.Summary) {
|
||||
@ -726,7 +726,7 @@ export default createReactClass({
|
||||
|
||||
_onJoinClick: async function() {
|
||||
if (this._matrixClient.isGuest()) {
|
||||
dis.dispatch({action: 'require_registration'});
|
||||
dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -821,10 +821,10 @@ export default createReactClass({
|
||||
{_t(
|
||||
"Want more than a community? <a>Get your own server</a>", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener">{sub}</a>,
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener">
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
|
||||
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
|
@ -161,6 +161,7 @@ export default createReactClass({
|
||||
_authStateUpdated: function(stageType, stageState) {
|
||||
const oldStage = this.state.authStage;
|
||||
this.setState({
|
||||
busy: false,
|
||||
authStage: stageType,
|
||||
stageState: stageState,
|
||||
errorText: stageState.error,
|
||||
@ -184,11 +185,13 @@ export default createReactClass({
|
||||
errorText: null,
|
||||
stageErrorText: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
}
|
||||
// The JS SDK eagerly reports itself as "not busy" right after any
|
||||
// immediate work has completed, but that's not really what we want at
|
||||
// the UI layer, so we ignore this signal and show a spinner until
|
||||
// there's a new screen to show the user. This is implemented by setting
|
||||
// `busy: false` in `_authStateUpdated`.
|
||||
// See also https://github.com/vector-im/riot-web/issues/12546
|
||||
},
|
||||
|
||||
_setFocus: function() {
|
||||
|
@ -337,13 +337,13 @@ const LoggedInView = createReactClass({
|
||||
|
||||
let handled = false;
|
||||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey ||
|
||||
ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
|
||||
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
case Key.PAGE_DOWN:
|
||||
if (!hasModifier) {
|
||||
if (!hasModifier && !isModifier) {
|
||||
this._onScrollKeyPressed(ev);
|
||||
handled = true;
|
||||
}
|
||||
@ -384,7 +384,10 @@ const LoggedInView = createReactClass({
|
||||
if (handled) {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
} else if (!hasModifier) {
|
||||
} else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
// The above condition is crafted to _allow_ characters with Shift
|
||||
// already pressed (but not the Shift key down itself).
|
||||
|
||||
const isClickShortcut = ev.target !== document.body &&
|
||||
(ev.key === Key.SPACE || ev.key === Key.ENTER);
|
||||
|
||||
@ -585,7 +588,8 @@ const LoggedInView = createReactClass({
|
||||
limitType={usageLimitEvent.getContent().limit_type}
|
||||
/>;
|
||||
} else if (this.props.showCookieBar &&
|
||||
this.props.config.piwik
|
||||
this.props.config.piwik &&
|
||||
navigator.doNotTrack !== "1"
|
||||
) {
|
||||
const policyUrl = this.props.config.piwik.policyUrl || null;
|
||||
topBar = <CookieBar policyUrl={policyUrl} />;
|
||||
|
@ -93,14 +93,19 @@ export default class MainSplit extends React.Component {
|
||||
const bodyView = React.Children.only(this.props.children);
|
||||
const panelView = this.props.panel;
|
||||
|
||||
if (this.props.collapsedRhs || !panelView) {
|
||||
return bodyView;
|
||||
} else {
|
||||
return <div className="mx_MainSplit" ref={this._setResizeContainerRef}>
|
||||
{ bodyView }
|
||||
const hasResizer = !this.props.collapsedRhs && panelView;
|
||||
|
||||
let children;
|
||||
if (hasResizer) {
|
||||
children = <React.Fragment>
|
||||
<ResizeHandle reverse={true} />
|
||||
{ panelView }
|
||||
</div>;
|
||||
</React.Fragment>;
|
||||
}
|
||||
|
||||
return <div className="mx_MainSplit" ref={hasResizer ? this._setResizeContainerRef : undefined}>
|
||||
{ bodyView }
|
||||
{ children }
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
@ -559,13 +559,19 @@ export default createReactClass({
|
||||
case 'view_user_info':
|
||||
this._viewUser(payload.userId, payload.subAction);
|
||||
break;
|
||||
case 'view_room':
|
||||
case 'view_room': {
|
||||
// Takes either a room ID or room alias: if switching to a room the client is already
|
||||
// known to be in (eg. user clicks on a room in the recents panel), supply the ID
|
||||
// If the user is clicking on a room in the context of the alias being presented
|
||||
// to them, supply the room alias. If both are supplied, the room ID will be ignored.
|
||||
this._viewRoom(payload);
|
||||
const promise = this._viewRoom(payload);
|
||||
if (payload.deferred_action) {
|
||||
promise.then(() => {
|
||||
dis.dispatch(payload.deferred_action);
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'view_prev_room':
|
||||
this._viewNextRoom(-1);
|
||||
break;
|
||||
@ -862,7 +868,7 @@ export default createReactClass({
|
||||
waitFor = this.firstSyncPromise.promise;
|
||||
}
|
||||
|
||||
waitFor.then(() => {
|
||||
return waitFor.then(() => {
|
||||
let presentedId = roomInfo.room_alias || roomInfo.room_id;
|
||||
const room = MatrixClientPeg.get().getRoom(roomInfo.room_id);
|
||||
if (room) {
|
||||
@ -885,7 +891,7 @@ export default createReactClass({
|
||||
presentedId += "/" + roomInfo.event_id;
|
||||
}
|
||||
newState.ready = true;
|
||||
this.setState(newState, ()=>{
|
||||
this.setState(newState, () => {
|
||||
this.notifyNewScreen('room/' + presentedId);
|
||||
});
|
||||
});
|
||||
@ -1008,6 +1014,10 @@ export default createReactClass({
|
||||
// needs to be reset so that they can revisit /user/.. // (and trigger
|
||||
// `_chatCreateOrReuse` again)
|
||||
go_welcome_on_cancel: true,
|
||||
screen_after: {
|
||||
screen: `user/${this.props.config.welcomeUserId}`,
|
||||
params: { action: 'chat' },
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -1175,8 +1185,17 @@ export default createReactClass({
|
||||
* Called when a new logged in session has started
|
||||
*/
|
||||
_onLoggedIn: async function() {
|
||||
ThemeController.isLogin = false;
|
||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||
// If a specific screen is set to be shown after login, show that above
|
||||
// all else, as it probably means the user clicked on something already.
|
||||
if (this._screenAfterLogin && this._screenAfterLogin.screen) {
|
||||
this.showScreen(
|
||||
this._screenAfterLogin.screen,
|
||||
this._screenAfterLogin.params,
|
||||
);
|
||||
this._screenAfterLogin = null;
|
||||
} else if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||
|
||||
if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) {
|
||||
@ -1374,7 +1393,8 @@ export default createReactClass({
|
||||
cancelButton: _t('Dismiss'),
|
||||
onFinished: (confirmed) => {
|
||||
if (confirmed) {
|
||||
window.open(consentUri, '_blank');
|
||||
const wnd = window.open(consentUri, '_blank');
|
||||
wnd.opener = null;
|
||||
}
|
||||
},
|
||||
}, null, true);
|
||||
@ -1475,26 +1495,29 @@ export default createReactClass({
|
||||
}
|
||||
});
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
} else {
|
||||
cli.on("crypto.verification.start", (verifier) => {
|
||||
cli.on("crypto.verification.request", request => {
|
||||
const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing");
|
||||
|
||||
if (!isFlagOn && !request.channel.deviceId) {
|
||||
request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"});
|
||||
return;
|
||||
}
|
||||
|
||||
if (request.verifier) {
|
||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
||||
verifier,
|
||||
verifier: request.verifier,
|
||||
}, null, /* priority = */ false, /* static = */ true);
|
||||
});
|
||||
}
|
||||
} else if (request.pending) {
|
||||
ToastStore.sharedInstance().addOrReplaceToast({
|
||||
key: 'verifreq_' + request.channel.transactionId,
|
||||
title: _t("Verification Request"),
|
||||
icon: "verification",
|
||||
props: {request},
|
||||
component: sdk.getComponent("toasts.VerificationRequestToast"),
|
||||
});
|
||||
}
|
||||
});
|
||||
// Fire the tinter right on startup to ensure the default theme is applied
|
||||
// A later sync can/will correct the tint to be the right value for the user
|
||||
const colorScheme = SettingsStore.getValue("roomColor");
|
||||
@ -1888,7 +1911,10 @@ export default createReactClass({
|
||||
// secret storage.
|
||||
SettingsStore.setFeatureEnabled("feature_cross_signing", true);
|
||||
this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY });
|
||||
} else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
} else if (
|
||||
SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")
|
||||
) {
|
||||
// This will only work if the feature is set to 'enable' in the config,
|
||||
// since it's too early in the lifecycle for users to have turned the
|
||||
// labs flag on.
|
||||
|
@ -28,6 +28,7 @@ import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import SettingsStore from '../../settings/SettingsStore';
|
||||
import {_t} from "../../languageHandler";
|
||||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
|
||||
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
|
||||
const continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
@ -873,6 +874,11 @@ class CreationGrouper {
|
||||
}
|
||||
|
||||
getTiles() {
|
||||
// If we don't have any events to group, don't even try to group them. The logic
|
||||
// below assumes that we have a group of events to deal with, but we might not if
|
||||
// the events we were supposed to group were redacted.
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||
|
||||
@ -950,15 +956,30 @@ class MemberGrouper {
|
||||
}
|
||||
|
||||
shouldGroup(ev) {
|
||||
if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) {
|
||||
return false;
|
||||
}
|
||||
return isMembershipChange(ev);
|
||||
}
|
||||
|
||||
add(ev) {
|
||||
if (ev.getType() === 'm.room.member') {
|
||||
// We'll just double check that it's worth our time to do so, through an
|
||||
// ugly hack. If textForEvent returns something, we should group it for
|
||||
// rendering but if it doesn't then we'll exclude it.
|
||||
const renderText = textForEvent(ev);
|
||||
if (!renderText || renderText.trim().length === 0) return; // quietly ignore
|
||||
}
|
||||
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
|
||||
this.events.push(ev);
|
||||
}
|
||||
|
||||
getTiles() {
|
||||
// If we don't have any events to group, don't even try to group them. The logic
|
||||
// below assumes that we have a group of events to deal with, but we might not if
|
||||
// the events we were supposed to group were redacted.
|
||||
if (!this.events || !this.events.length) return [];
|
||||
|
||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||
|
||||
|
@ -182,6 +182,7 @@ export default class RightPanel extends React.Component {
|
||||
member: payload.member,
|
||||
event: payload.event,
|
||||
verificationRequest: payload.verificationRequest,
|
||||
verificationRequestPromise: payload.verificationRequestPromise,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -231,6 +232,7 @@ export default class RightPanel extends React.Component {
|
||||
onClose={onClose}
|
||||
phase={this.state.phase}
|
||||
verificationRequest={this.state.verificationRequest}
|
||||
verificationRequestPromise={this.state.verificationRequestPromise}
|
||||
/>;
|
||||
} else {
|
||||
panel = <MemberInfo
|
||||
|
@ -406,6 +406,12 @@ export default createReactClass({
|
||||
// would normally decide what the name is.
|
||||
name: room.name || room_alias || _t('Unnamed room'),
|
||||
};
|
||||
|
||||
if (this.state.roomServer) {
|
||||
payload.opts = {
|
||||
viaServers: [this.state.roomServer],
|
||||
};
|
||||
}
|
||||
}
|
||||
// It's not really possible to join Matrix rooms by ID because the HS has no way to know
|
||||
// which servers to start querying. However, there's no other way to join rooms in
|
||||
|
@ -46,8 +46,6 @@ export default class RoomSubList extends React.PureComponent {
|
||||
tagName: PropTypes.string,
|
||||
addRoomLabel: PropTypes.string,
|
||||
|
||||
order: PropTypes.string.isRequired,
|
||||
|
||||
// passed through to RoomTile and used to highlight room with `!` regardless of notifications count
|
||||
isInvite: PropTypes.bool,
|
||||
|
||||
|
@ -342,7 +342,7 @@ export default createReactClass({
|
||||
peekLoading: false,
|
||||
});
|
||||
this._onRoomLoaded(room);
|
||||
}, (err) => {
|
||||
}).catch((err) => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
@ -355,7 +355,7 @@ export default createReactClass({
|
||||
// This won't necessarily be a MatrixError, but we duck-type
|
||||
// here and say if it's got an 'errcode' key with the right value,
|
||||
// it means we can't peek.
|
||||
if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") {
|
||||
if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === 'M_FORBIDDEN') {
|
||||
// This is fine: the room just isn't peekable (we assume).
|
||||
this.setState({
|
||||
peekLoading: false,
|
||||
@ -365,8 +365,6 @@ export default createReactClass({
|
||||
}
|
||||
});
|
||||
} else if (room) {
|
||||
//viewing a previously joined room, try to lazy load members
|
||||
|
||||
// Stop peeking because we have joined this room previously
|
||||
MatrixClientPeg.get().stopPeeking();
|
||||
this.setState({isPeeking: false});
|
||||
@ -460,8 +458,6 @@ export default createReactClass({
|
||||
// (We could use isMounted, but facebook have deprecated that.)
|
||||
this.unmounted = true;
|
||||
|
||||
SettingsStore.unwatchSetting(this._ciderWatcherRef);
|
||||
|
||||
// update the scroll map before we get unmounted
|
||||
if (this.state.roomId) {
|
||||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||
@ -618,6 +614,22 @@ export default createReactClass({
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
break;
|
||||
case 'quote':
|
||||
if (this.state.searchResults) {
|
||||
const roomId = payload.event.getRoomId();
|
||||
if (roomId === this.state.roomId) {
|
||||
this.onCancelSearchClick();
|
||||
}
|
||||
|
||||
setImmediate(() => {
|
||||
dis.dispatch({
|
||||
action: 'view_room',
|
||||
room_id: roomId,
|
||||
deferred_action: payload,
|
||||
});
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -523,7 +523,7 @@ export default createReactClass({
|
||||
scrollRelative: function(mult) {
|
||||
const scrollNode = this._getScrollNode();
|
||||
const delta = mult * scrollNode.clientHeight * 0.5;
|
||||
scrollNode.scrollTop = scrollNode.scrollTop + delta;
|
||||
scrollNode.scrollBy(0, delta);
|
||||
this._saveScrollState();
|
||||
},
|
||||
|
||||
@ -705,17 +705,15 @@ export default createReactClass({
|
||||
// the currently filled piece of the timeline
|
||||
if (trackedNode) {
|
||||
const oldTop = trackedNode.offsetTop;
|
||||
// changing the height might change the scrollTop
|
||||
// if the new height is smaller than the scrollTop.
|
||||
// We calculate the diff that needs to be applied
|
||||
// ourselves, so be sure to measure the
|
||||
// scrollTop before changing the height.
|
||||
const preexistingScrollTop = sn.scrollTop;
|
||||
itemlist.style.height = `${newHeight}px`;
|
||||
const newTop = trackedNode.offsetTop;
|
||||
const topDiff = newTop - oldTop;
|
||||
sn.scrollTop = preexistingScrollTop + topDiff;
|
||||
debuglog("updateHeight to", {newHeight, topDiff, preexistingScrollTop});
|
||||
// important to scroll by a relative amount as
|
||||
// reading scrollTop and then setting it might
|
||||
// yield out of date values and cause a jump
|
||||
// when setting it
|
||||
sn.scrollBy(0, topDiff);
|
||||
debuglog("updateHeight to", {newHeight, topDiff});
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -767,6 +765,7 @@ export default createReactClass({
|
||||
},
|
||||
|
||||
_topFromBottom(node) {
|
||||
// current capped height - distance from top = distance from bottom of container to top of tracked element
|
||||
return this._itemlist.current.clientHeight - node.offsetTop;
|
||||
},
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Travis Ralston
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 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.
|
||||
@ -18,41 +18,54 @@ limitations under the License.
|
||||
|
||||
import * as React from "react";
|
||||
import {_t} from '../../languageHandler';
|
||||
import PropTypes from "prop-types";
|
||||
import * as PropTypes from "prop-types";
|
||||
import * as sdk from "../../index";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
/**
|
||||
* Represents a tab for the TabbedView.
|
||||
*/
|
||||
export class Tab {
|
||||
public label: string;
|
||||
public icon: string;
|
||||
public body: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Creates a new tab.
|
||||
* @param {string} tabLabel The untranslated tab label.
|
||||
* @param {string} tabIconClass The class for the tab icon. This should be a simple mask.
|
||||
* @param {string} tabJsx The JSX for the tab container.
|
||||
* @param {React.ReactNode} tabJsx The JSX for the tab container.
|
||||
*/
|
||||
constructor(tabLabel, tabIconClass, tabJsx) {
|
||||
constructor(tabLabel: string, tabIconClass: string, tabJsx: React.ReactNode) {
|
||||
this.label = tabLabel;
|
||||
this.icon = tabIconClass;
|
||||
this.body = tabJsx;
|
||||
}
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component {
|
||||
interface IProps {
|
||||
tabs: Tab[];
|
||||
}
|
||||
|
||||
interface IState {
|
||||
activeTabIndex: number;
|
||||
}
|
||||
|
||||
export default class TabbedView extends React.Component<IProps, IState> {
|
||||
static propTypes = {
|
||||
// The tabs to show
|
||||
tabs: PropTypes.arrayOf(PropTypes.instanceOf(Tab)).isRequired,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
constructor(props: IProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeTabIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
_getActiveTabIndex() {
|
||||
private _getActiveTabIndex() {
|
||||
if (!this.state || !this.state.activeTabIndex) return 0;
|
||||
return this.state.activeTabIndex;
|
||||
}
|
||||
@ -62,7 +75,7 @@ export default class TabbedView extends React.Component {
|
||||
* @param {Tab} tab the tab to show
|
||||
* @private
|
||||
*/
|
||||
_setActiveTab(tab) {
|
||||
private _setActiveTab(tab: Tab) {
|
||||
const idx = this.props.tabs.indexOf(tab);
|
||||
if (idx !== -1) {
|
||||
this.setState({activeTabIndex: idx});
|
||||
@ -71,7 +84,7 @@ export default class TabbedView extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
_renderTabLabel(tab) {
|
||||
private _renderTabLabel(tab: Tab) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
let classes = "mx_TabbedView_tabLabel ";
|
||||
@ -97,7 +110,7 @@ export default class TabbedView extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
_renderTabPanel(tab) {
|
||||
private _renderTabPanel(tab: Tab): React.ReactNode {
|
||||
return (
|
||||
<div className="mx_TabbedView_tabPanel" key={"mx_tabpanel_" + tab.label}>
|
||||
<div className='mx_TabbedView_tabPanelContent'>
|
||||
@ -107,7 +120,7 @@ export default class TabbedView extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
public render(): React.ReactNode {
|
||||
const labels = this.props.tabs.map(tab => this._renderTabLabel(tab));
|
||||
const panel = this._renderTabPanel(this.props.tabs[this._getActiveTabIndex()]);
|
||||
|
@ -54,17 +54,39 @@ export default class CompleteSecurity extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
onStartClick = async () => {
|
||||
_onUsePassphraseClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_BUSY,
|
||||
});
|
||||
const cli = MatrixClientPeg.get();
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
this.setState({backupInfo});
|
||||
try {
|
||||
await accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
if (backupInfo) await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
const backupInfo = await cli.getKeyBackupVersion();
|
||||
this.setState({backupInfo});
|
||||
|
||||
// The control flow is fairly twisted here...
|
||||
// For the purposes of completing security, we only wait on getting
|
||||
// as far as the trust check and then show a green shield.
|
||||
// We also begin the key backup restore as well, which we're
|
||||
// awaiting inside `accessSecretStorage` only so that it keeps your
|
||||
// passphase cached for that work. This dialog itself will only wait
|
||||
// on the first trust check, and the key backup restore will happen
|
||||
// in the background.
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
resolve();
|
||||
if (backupInfo) {
|
||||
// A complete restore can take many minutes for large
|
||||
// accounts / slow servers, so we allow the dialog
|
||||
// to advance before this.
|
||||
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
|
||||
if (cli.getCrossSigningId()) {
|
||||
@ -147,13 +169,27 @@ export default class CompleteSecurity extends React.Component {
|
||||
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||
/>;
|
||||
} else if (phase === PHASE_INTRO) {
|
||||
const InlineSpinner = sdk.getComponent('elements.InlineSpinner');
|
||||
|
||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||
title = _t("Complete security");
|
||||
body = (
|
||||
<div>
|
||||
<p>{_t(
|
||||
"Verify this session to grant it access to encrypted messages.",
|
||||
"Open an existing session & use it to verify this one, " +
|
||||
"granting it access to encrypted messages.",
|
||||
)}</p>
|
||||
<p className="mx_CompleteSecurity_waiting"><InlineSpinner />{_t("Waiting…")}</p>
|
||||
<p>{_t(
|
||||
"If you can’t access one, <button>use your recovery key or passphrase.</button>",
|
||||
{}, {
|
||||
button: sub => <AccessibleButton element="span"
|
||||
className="mx_linkButton"
|
||||
onClick={this._onUsePassphraseClick}
|
||||
>
|
||||
{sub}
|
||||
</AccessibleButton>,
|
||||
})}</p>
|
||||
<div className="mx_CompleteSecurity_actionRow">
|
||||
<AccessibleButton
|
||||
kind="danger"
|
||||
@ -161,12 +197,6 @@ export default class CompleteSecurity extends React.Component {
|
||||
>
|
||||
{_t("Skip")}
|
||||
</AccessibleButton>
|
||||
<AccessibleButton
|
||||
kind="primary"
|
||||
onClick={this.onStartClick}
|
||||
>
|
||||
{_t("Start")}
|
||||
</AccessibleButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -27,6 +27,8 @@ import { messageForResourceLimitError } from '../../../utils/ErrorUtils';
|
||||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from "classnames";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
import PlatformPeg from '../../../PlatformPeg';
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
@ -120,8 +122,8 @@ export default createReactClass({
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
|
||||
// CAS and SSO are the same thing, modulo the url we link to
|
||||
'm.login.cas': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("cas")),
|
||||
'm.login.sso': () => this._renderSsoStep(this._loginLogic.getSsoLoginUrl("sso")),
|
||||
'm.login.cas': () => this._renderSsoStep("cas"),
|
||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
||||
};
|
||||
|
||||
this._initLoginLogic();
|
||||
@ -245,6 +247,7 @@ export default createReactClass({
|
||||
}
|
||||
|
||||
this.setState({
|
||||
busy: false,
|
||||
errorText: errorText,
|
||||
// 401 would be the sensible status code for 'incorrect password'
|
||||
// but the login API gives a 403 https://matrix.org/jira/browse/SYN-744
|
||||
@ -252,13 +255,6 @@ export default createReactClass({
|
||||
// We treat both as an incorrect password
|
||||
loginIncorrect: error.httpStatus === 401 || error.httpStatus === 403,
|
||||
});
|
||||
}).finally(() => {
|
||||
if (this._unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
busy: false,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@ -344,6 +340,21 @@ export default createReactClass({
|
||||
this.props.onRegisterClick();
|
||||
},
|
||||
|
||||
onTryRegisterClick: function(ev) {
|
||||
const step = this._getCurrentFlowStep();
|
||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
||||
// If we're showing SSO it means that registration is also probably disabled,
|
||||
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
|
||||
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind);
|
||||
} else {
|
||||
// Don't intercept - just go through to the register page
|
||||
this.onRegisterClick(ev);
|
||||
}
|
||||
},
|
||||
|
||||
async onServerDetailsNextPhaseClick() {
|
||||
this.setState({
|
||||
phase: PHASE_LOGIN,
|
||||
@ -481,7 +492,7 @@ export default createReactClass({
|
||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener"
|
||||
return <a target="_blank" rel="noreferrer noopener"
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
>
|
||||
{ sub }
|
||||
@ -496,11 +507,10 @@ export default createReactClass({
|
||||
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
|
||||
'a': (sub) =>
|
||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
</span>;
|
||||
@ -586,7 +596,7 @@ export default createReactClass({
|
||||
);
|
||||
},
|
||||
|
||||
_renderSsoStep: function(url) {
|
||||
_renderSsoStep: function(loginType) {
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
let onEditServerDetailsClick = null;
|
||||
@ -607,7 +617,10 @@ export default createReactClass({
|
||||
<SignInToText serverConfig={this.props.serverConfig}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick} />
|
||||
|
||||
<a href={url} className="mx_Login_sso_link mx_Login_submit">{ _t('Sign in with single sign-on') }</a>
|
||||
<SSOButton
|
||||
className="mx_Login_sso_link mx_Login_submit"
|
||||
matrixClient={this._loginLogic.createTemporaryClient()}
|
||||
loginType={loginType} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@ -655,7 +668,7 @@ export default createReactClass({
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.renderLoginComponentForStep() }
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onRegisterClick} href="#">
|
||||
<a className="mx_AuthBody_changeFlow" onClick={this.onTryRegisterClick} href="#">
|
||||
{ _t('Create account') }
|
||||
</a>
|
||||
</AuthBody>
|
||||
|
@ -31,6 +31,8 @@ import classNames from "classnames";
|
||||
import * as Lifecycle from '../../../Lifecycle';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import Login from "../../../Login";
|
||||
import dis from "../../../dispatcher";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
@ -232,6 +234,13 @@ export default createReactClass({
|
||||
serverRequiresIdServer,
|
||||
busy: false,
|
||||
});
|
||||
const showGenericError = (e) => {
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
};
|
||||
try {
|
||||
await this._makeRegisterRequest({});
|
||||
// This should never succeed since we specified an empty
|
||||
@ -243,18 +252,32 @@ export default createReactClass({
|
||||
flows: e.data.flows,
|
||||
});
|
||||
} else if (e.httpStatus === 403 && e.errcode === "M_UNKNOWN") {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
// At this point registration is pretty much disabled, but before we do that let's
|
||||
// quickly check to see if the server supports SSO instead. If it does, we'll send
|
||||
// the user off to the login page to figure their account out.
|
||||
try {
|
||||
const loginLogic = new Login(hsUrl, isUrl, null, {
|
||||
defaultDeviceDisplayName: "riot login check", // We shouldn't ever be used
|
||||
});
|
||||
const flows = await loginLogic.getFlows();
|
||||
const hasSsoFlow = flows.find(f => f.type === 'm.login.sso' || f.type === 'm.login.cas');
|
||||
if (hasSsoFlow) {
|
||||
// Redirect to login page - server probably expects SSO only
|
||||
dis.dispatch({action: 'start_login'});
|
||||
} else {
|
||||
this.setState({
|
||||
errorText: _t("Registration has been disabled on this homeserver."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to get login flows to check for SSO support", e);
|
||||
showGenericError(e);
|
||||
}
|
||||
} else {
|
||||
console.log("Unable to query for supported registration methods.", e);
|
||||
this.setState({
|
||||
errorText: _t("Unable to query for supported registration methods."),
|
||||
// add empty flows array to get rid of spinner
|
||||
flows: [],
|
||||
});
|
||||
showGenericError(e);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -23,8 +23,8 @@ import * as Lifecycle from '../../../Lifecycle';
|
||||
import Modal from '../../../Modal';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import {sendLoginRequest} from "../../../Login";
|
||||
import url from 'url';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import SSOButton from "../../views/elements/SSOButton";
|
||||
|
||||
const LOGIN_VIEW = {
|
||||
LOADING: 1,
|
||||
@ -55,7 +55,6 @@ export default class SoftLogout extends React.Component {
|
||||
this.state = {
|
||||
loginView: LOGIN_VIEW.LOADING,
|
||||
keyBackupNeeded: true, // assume we do while we figure it out (see componentWillMount)
|
||||
ssoUrl: null,
|
||||
|
||||
busy: false,
|
||||
password: "",
|
||||
@ -105,18 +104,6 @@ export default class SoftLogout extends React.Component {
|
||||
|
||||
const chosenView = loginViews.filter(f => !!f)[0] || LOGIN_VIEW.UNSUPPORTED;
|
||||
this.setState({loginView: chosenView});
|
||||
|
||||
if (chosenView === LOGIN_VIEW.CAS || chosenView === LOGIN_VIEW.SSO) {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
const appUrl = url.parse(window.location.href, true);
|
||||
appUrl.hash = ""; // Clear #/soft_logout off the URL
|
||||
appUrl.query["homeserver"] = client.getHomeserverUrl();
|
||||
appUrl.query["identityServer"] = client.getIdentityServerUrl();
|
||||
|
||||
const ssoUrl = client.getSsoLoginUrl(url.format(appUrl), chosenView === LOGIN_VIEW.CAS ? "cas" : "sso");
|
||||
this.setState({ssoUrl});
|
||||
}
|
||||
}
|
||||
|
||||
onPasswordChange = (ev) => {
|
||||
@ -195,14 +182,6 @@ export default class SoftLogout extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSsoLogin = async (ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({busy: true});
|
||||
window.location.href = this.state.ssoUrl;
|
||||
};
|
||||
|
||||
_renderSignInSection() {
|
||||
if (this.state.loginView === LOGIN_VIEW.LOADING) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
@ -257,8 +236,6 @@ export default class SoftLogout extends React.Component {
|
||||
}
|
||||
|
||||
if (this.state.loginView === LOGIN_VIEW.SSO || this.state.loginView === LOGIN_VIEW.CAS) {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
||||
if (!introText) {
|
||||
introText = _t("Sign in and regain access to your account.");
|
||||
} // else we already have a message and should use it (key backup warning)
|
||||
@ -266,9 +243,9 @@ export default class SoftLogout extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
<p>{introText}</p>
|
||||
<AccessibleButton kind='primary' onClick={this.onSsoLogin}>
|
||||
{_t('Sign in with single sign-on')}
|
||||
</AccessibleButton>
|
||||
<SSOButton
|
||||
matrixClient={MatrixClientPeg.get()}
|
||||
loginType={this.state.loginView === LOGIN_VIEW.CAS ? "cas" : "sso"} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export default createReactClass({
|
||||
render: function() {
|
||||
return (
|
||||
<div className="mx_AuthFooter">
|
||||
<a href="https://matrix.org" target="_blank" rel="noopener">{ _t("powered by Matrix") }</a>
|
||||
<a href="https://matrix.org" target="_blank" rel="noreferrer noopener">{ _t("powered by Matrix") }</a>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
@ -331,7 +331,7 @@ export const TermsAuthEntry = createReactClass({
|
||||
checkboxes.push(
|
||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
||||
<a href={policy.url} target="_blank" rel="noopener">{ policy.name }</a>
|
||||
<a href={policy.url} target="_blank" rel="noreferrer noopener">{ policy.name }</a>
|
||||
</label>,
|
||||
);
|
||||
}
|
||||
@ -604,6 +604,7 @@ export const FallbackAuthEntry = createReactClass({
|
||||
this.props.authSessionId,
|
||||
);
|
||||
this._popupWindow = window.open(url);
|
||||
this._popupWindow.opener = null;
|
||||
},
|
||||
|
||||
_onReceiveMessage: function(event) {
|
||||
|
@ -99,7 +99,7 @@ export default class ModularServerConfig extends ServerConfig {
|
||||
"Enter the location of your Modular homeserver. It may use your own " +
|
||||
"domain name or be a subdomain of <a>modular.im</a>.",
|
||||
{}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
},
|
||||
|
@ -46,7 +46,7 @@ export const TYPES = {
|
||||
label: () => _t('Premium'),
|
||||
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noopener">
|
||||
a: sub => <a href={MODULAR_URL} target="_blank" rel="noreferrer noopener">
|
||||
{sub}
|
||||
</a>,
|
||||
}),
|
||||
|
@ -90,7 +90,8 @@ export default createReactClass({
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId());
|
||||
const pinnedEvent = room.currentState.getStateEvents('m.room.pinned_events', '');
|
||||
if (!pinnedEvent) return false;
|
||||
return pinnedEvent.getContent().pinned.includes(this.props.mxEvent.getId());
|
||||
const content = pinnedEvent.getContent();
|
||||
return content.pinned && Array.isArray(content.pinned) && content.pinned.includes(this.props.mxEvent.getId());
|
||||
},
|
||||
|
||||
onResendClick: function() {
|
||||
@ -420,7 +421,7 @@ export default createReactClass({
|
||||
onClick={this.onPermalinkClick}
|
||||
href={permalink}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||
? _t('Share Permalink') : _t('Share Message') }
|
||||
@ -445,7 +446,7 @@ export default createReactClass({
|
||||
element="a"
|
||||
className="mx_MessageContextMenu_field"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
rel="noreferrer noopener"
|
||||
onClick={this.closeMenu}
|
||||
href={mxEvent.event.content.external_url}
|
||||
>
|
||||
|
@ -68,10 +68,11 @@ export default class TopLeftMenu extends React.Component {
|
||||
{_t(
|
||||
"<a>Upgrade</a> to your own domain", {},
|
||||
{
|
||||
a: sub => <a href={hostingSignupLink} target="_blank" rel="noopener" tabIndex={-1}>{sub}</a>,
|
||||
a: sub =>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" tabIndex={-1}>{sub}</a>,
|
||||
},
|
||||
)}
|
||||
<a href={hostingSignupLink} target="_blank" rel="noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener" role="presentation" aria-hidden={true} tabIndex={-1}>
|
||||
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||
</a>
|
||||
</div>;
|
||||
|
@ -72,7 +72,7 @@ export default createReactClass({
|
||||
<button onClick={this._onInviteNeverWarnClicked}>
|
||||
{ _t('Invite anyway and never warn me again') }
|
||||
</button>
|
||||
<button onClick={this._onInviteClicked} autoFocus="true">
|
||||
<button onClick={this._onInviteClicked} autoFocus={true}>
|
||||
{ _t('Invite anyway') }
|
||||
</button>
|
||||
</div>
|
||||
|
@ -52,7 +52,7 @@ export default class ChangelogDialog extends React.Component {
|
||||
_elementsForCommit(commit) {
|
||||
return (
|
||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||
<a href={commit.html_url} target="_blank" rel="noopener">
|
||||
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||
{commit.commit.message.split('\n')[0]}
|
||||
</a>
|
||||
</li>
|
||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, {useState, useEffect} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as sdk from '../../../index';
|
||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||
@ -22,6 +22,16 @@ import { _t } from '../../../languageHandler';
|
||||
import { Room } from "matrix-js-sdk";
|
||||
import Field from "../elements/Field";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||
|
||||
import {
|
||||
PHASE_UNSENT,
|
||||
PHASE_REQUESTED,
|
||||
PHASE_READY,
|
||||
PHASE_DONE,
|
||||
PHASE_STARTED,
|
||||
PHASE_CANCELLED,
|
||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
|
||||
class GenericEditor extends React.PureComponent {
|
||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
||||
@ -605,12 +615,97 @@ class ServersInRoomList extends React.PureComponent {
|
||||
}
|
||||
}
|
||||
|
||||
const PHASE_MAP = {
|
||||
[PHASE_UNSENT]: "unsent",
|
||||
[PHASE_REQUESTED]: "requested",
|
||||
[PHASE_READY]: "ready",
|
||||
[PHASE_DONE]: "done",
|
||||
[PHASE_STARTED]: "started",
|
||||
[PHASE_CANCELLED]: "cancelled",
|
||||
};
|
||||
|
||||
function VerificationRequest({txnId, request}) {
|
||||
const [, updateState] = useState();
|
||||
const [timeout, setRequestTimeout] = useState(request.timeout);
|
||||
|
||||
/* Re-render if something changes state */
|
||||
useEventEmitter(request, "change", updateState);
|
||||
|
||||
/* Keep re-rendering if there's a timeout */
|
||||
useEffect(() => {
|
||||
if (request.timeout == 0) return;
|
||||
|
||||
/* Note that request.timeout is a getter, so its value changes */
|
||||
const id = setInterval(() => {
|
||||
setRequestTimeout(request.timeout);
|
||||
}, 500);
|
||||
|
||||
return () => { clearInterval(id); };
|
||||
}, [request]);
|
||||
|
||||
return (<div className="mx_DevTools_VerificationRequest">
|
||||
<dl>
|
||||
<dt>Transaction</dt>
|
||||
<dd>{txnId}</dd>
|
||||
<dt>Phase</dt>
|
||||
<dd>{PHASE_MAP[request.phase] || request.phase}</dd>
|
||||
<dt>Timeout</dt>
|
||||
<dd>{Math.floor(timeout / 1000)}</dd>
|
||||
<dt>Methods</dt>
|
||||
<dd>{request.methods && request.methods.join(", ")}</dd>
|
||||
<dt>requestingUserId</dt>
|
||||
<dd>{request.requestingUserId}</dd>
|
||||
<dt>observeOnly</dt>
|
||||
<dd>{JSON.stringify(request.observeOnly)}</dd>
|
||||
</dl>
|
||||
</div>);
|
||||
}
|
||||
|
||||
class VerificationExplorer extends React.Component {
|
||||
static getLabel() {
|
||||
return _t("Verification Requests");
|
||||
}
|
||||
|
||||
/* Ensure this.context is the cli */
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
onNewRequest = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const cli = this.context;
|
||||
cli.on("crypto.verification.request", this.onNewRequest);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const cli = this.context;
|
||||
cli.off("crypto.verification.request", this.onNewRequest);
|
||||
}
|
||||
|
||||
render() {
|
||||
const cli = this.context;
|
||||
const room = this.props.room;
|
||||
const inRoomChannel = cli._crypto._inRoomVerificationRequests;
|
||||
const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map();
|
||||
|
||||
return (<div>
|
||||
<div className="mx_Dialog_content">
|
||||
{Array.from(inRoomRequests.entries()).reverse().map(([txnId, request]) =>
|
||||
<VerificationRequest txnId={txnId} request={request} key={txnId} />,
|
||||
)}
|
||||
</div>
|
||||
</div>);
|
||||
}
|
||||
}
|
||||
|
||||
const Entries = [
|
||||
SendCustomEvent,
|
||||
RoomStateExplorer,
|
||||
SendAccountData,
|
||||
AccountDataExplorer,
|
||||
ServersInRoomList,
|
||||
VerificationExplorer,
|
||||
];
|
||||
|
||||
export default class DevtoolsDialog extends React.PureComponent {
|
||||
|
@ -34,6 +34,7 @@ import {humanizeTime} from "../../../utils/humanize";
|
||||
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||
import SettingsStore from '../../../settings/SettingsStore';
|
||||
import RoomListStore, {TAG_DM} from "../../../stores/RoomListStore";
|
||||
|
||||
export const KIND_DM = "dm";
|
||||
export const KIND_INVITE = "invite";
|
||||
@ -218,7 +219,7 @@ class DMRoomTile extends React.PureComponent {
|
||||
}
|
||||
|
||||
// Push any text we missed (end of text)
|
||||
if (i < (str.length - 1)) {
|
||||
if (i < str.length) {
|
||||
result.push(<span key={i + 'end'}>{str.substring(i)}</span>);
|
||||
}
|
||||
|
||||
@ -332,7 +333,23 @@ export default class InviteDialog extends React.PureComponent {
|
||||
}
|
||||
|
||||
_buildRecents(excludedTargetIds: Set<string>): {userId: string, user: RoomMember, lastActive: number} {
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals();
|
||||
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
|
||||
|
||||
// Also pull in all the rooms tagged as TAG_DM so we don't miss anything. Sometimes the
|
||||
// room list doesn't tag the room for the DMRoomMap, but does for the room list.
|
||||
const taggedRooms = RoomListStore.getRoomLists();
|
||||
const dmTaggedRooms = taggedRooms[TAG_DM];
|
||||
const myUserId = MatrixClientPeg.get().getUserId();
|
||||
for (const dmRoom of dmTaggedRooms) {
|
||||
const otherMembers = dmRoom.getJoinedMembers().filter(u => u.userId !== myUserId);
|
||||
for (const member of otherMembers) {
|
||||
if (rooms[member.userId]) continue; // already have a room
|
||||
|
||||
console.warn(`Adding DM room for ${member.userId} as ${dmRoom.roomId} from tag, not DM map`);
|
||||
rooms[member.userId] = dmRoom;
|
||||
}
|
||||
}
|
||||
|
||||
const recents = [];
|
||||
for (const userId in rooms) {
|
||||
// Filter out user IDs that are already in the room / should be excluded
|
||||
@ -512,9 +529,27 @@ export default class InviteDialog extends React.PureComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
_convertFilter(): Member[] {
|
||||
// Check to see if there's anything to convert first
|
||||
if (!this.state.filterText || !this.state.filterText.includes('@')) return this.state.targets || [];
|
||||
|
||||
let newMember: Member;
|
||||
if (this.state.filterText.startsWith('@')) {
|
||||
// Assume mxid
|
||||
newMember = new DirectoryMember({user_id: this.state.filterText, display_name: null, avatar_url: null});
|
||||
} else {
|
||||
// Assume email
|
||||
newMember = new ThreepidMember(this.state.filterText);
|
||||
}
|
||||
const newTargets = [...(this.state.targets || []), newMember];
|
||||
this.setState({targets: newTargets, filterText: ''});
|
||||
return newTargets;
|
||||
}
|
||||
|
||||
_startDm = async () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
// Check if there is already a DM with these people and reuse it if possible.
|
||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||
@ -529,7 +564,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
const createRoomOptions = {};
|
||||
const createRoomOptions = {inlineErrors: true};
|
||||
|
||||
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||
// Check whether all users have uploaded device keys before.
|
||||
@ -544,9 +579,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||
// Check if it's a traditional DM and create the room if required.
|
||||
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||
let createRoomPromise = Promise.resolve();
|
||||
if (targetIds.length === 1) {
|
||||
const isSelf = targetIds.length === 1 && targetIds[0] === MatrixClientPeg.get().getUserId();
|
||||
if (targetIds.length === 1 && !isSelf) {
|
||||
createRoomOptions.dmUserId = targetIds[0];
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else if (isSelf) {
|
||||
createRoomPromise = createRoom(createRoomOptions);
|
||||
} else {
|
||||
// Create a boring room and try to invite the targets manually.
|
||||
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
||||
@ -573,7 +611,9 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
_inviteUsers = () => {
|
||||
this.setState({busy: true});
|
||||
const targetIds = this.state.targets.map(t => t.userId);
|
||||
this._convertFilter();
|
||||
const targets = this._convertFilter();
|
||||
const targetIds = targets.map(t => t.userId);
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||
if (!room) {
|
||||
@ -630,13 +670,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
// While we're here, try and autocomplete a search result for the mxid itself
|
||||
// if there's no matches (and the input looks like a mxid).
|
||||
if (term[0] === '@' && term.indexOf(':') > 1 && r.results.length === 0) {
|
||||
if (term[0] === '@' && term.indexOf(':') > 1) {
|
||||
try {
|
||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||
if (profile) {
|
||||
// If we have a profile, we have enough information to assume that
|
||||
// the mxid can be invited - add it to the list
|
||||
r.results.push({
|
||||
// the mxid can be invited - add it to the list. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: profile['displayname'],
|
||||
avatar_url: profile['avatar_url'],
|
||||
@ -645,6 +686,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||
} catch (e) {
|
||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||
console.warn(e);
|
||||
|
||||
// Add a result anyways, just without a profile. We stick it at the
|
||||
// top so it is most obviously presented to the user.
|
||||
r.results.splice(0, 0, {
|
||||
user_id: term,
|
||||
display_name: term,
|
||||
avatar_url: null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -769,7 +818,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
];
|
||||
const toAdd = [];
|
||||
const failed = [];
|
||||
const potentialAddresses = text.split(/[\s,]+/);
|
||||
const potentialAddresses = text.split(/[\s,]+/).map(p => p.trim()).filter(p => !!p); // filter empty strings
|
||||
for (const address of potentialAddresses) {
|
||||
const member = possibleMembers.find(m => m.userId === address);
|
||||
if (member) {
|
||||
@ -857,24 +906,24 @@ export default class InviteDialog extends React.PureComponent {
|
||||
// Mix in the server results if we have any, but only if we're searching. We track the additional
|
||||
// members separately because we want to filter sourceMembers but trust the mixin arrays to have
|
||||
// the right members in them.
|
||||
let additionalMembers = [];
|
||||
let priorityAdditionalMembers = []; // Shows up before our own suggestions, higher quality
|
||||
let otherAdditionalMembers = []; // Shows up after our own suggestions, lower quality
|
||||
const hasMixins = this.state.serverResultsMixin || this.state.threepidResultsMixin;
|
||||
if (this.state.filterText && hasMixins && kind === 'suggestions') {
|
||||
// We don't want to duplicate members though, so just exclude anyone we've already seen.
|
||||
const notAlreadyExists = (u: Member): boolean => {
|
||||
return !sourceMembers.some(m => m.userId === u.userId)
|
||||
&& !additionalMembers.some(m => m.userId === u.userId);
|
||||
&& !priorityAdditionalMembers.some(m => m.userId === u.userId)
|
||||
&& !otherAdditionalMembers.some(m => m.userId === u.userId);
|
||||
};
|
||||
|
||||
const uniqueServerResults = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueServerResults);
|
||||
|
||||
const uniqueThreepidResults = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
additionalMembers = additionalMembers.concat(...uniqueThreepidResults);
|
||||
otherAdditionalMembers = this.state.serverResultsMixin.filter(notAlreadyExists);
|
||||
priorityAdditionalMembers = this.state.threepidResultsMixin.filter(notAlreadyExists);
|
||||
}
|
||||
const hasAdditionalMembers = priorityAdditionalMembers.length > 0 || otherAdditionalMembers.length > 0;
|
||||
|
||||
// Hide the section if there's nothing to filter by
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) return null;
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) return null;
|
||||
|
||||
// Do some simple filtering on the input before going much further. If we get no results, say so.
|
||||
if (this.state.filterText) {
|
||||
@ -882,7 +931,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
sourceMembers = sourceMembers
|
||||
.filter(m => m.user.name.toLowerCase().includes(filterBy) || m.userId.toLowerCase().includes(filterBy));
|
||||
|
||||
if (sourceMembers.length === 0 && additionalMembers.length === 0) {
|
||||
if (sourceMembers.length === 0 && !hasAdditionalMembers) {
|
||||
return (
|
||||
<div className='mx_InviteDialog_section'>
|
||||
<h3>{sectionName}</h3>
|
||||
@ -894,7 +943,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
|
||||
// Now we mix in the additional members. Again, we presume these have already been filtered. We
|
||||
// also assume they are more relevant than our suggestions and prepend them to the list.
|
||||
sourceMembers = [...additionalMembers, ...sourceMembers];
|
||||
sourceMembers = [...priorityAdditionalMembers, ...sourceMembers, ...otherAdditionalMembers];
|
||||
|
||||
// If we're going to hide one member behind 'show more', just use up the space of the button
|
||||
// with the member's tile instead.
|
||||
@ -1014,7 +1063,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
"If you can't find someone, ask them for their username, share your " +
|
||||
"username (%(userId)s) or <a>profile link</a>.",
|
||||
{userId},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
{a: (sub) => <a href={makeUserPermalink(userId)} rel="noreferrer noopener" target="_blank">{sub}</a>},
|
||||
);
|
||||
buttonText = _t("Go");
|
||||
goButtonFn = this._startDm;
|
||||
@ -1023,12 +1072,17 @@ export default class InviteDialog extends React.PureComponent {
|
||||
helpText = _t(
|
||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
||||
"<a>share this room</a>.", {},
|
||||
{a: (sub) => <a href={makeRoomPermalink(this.props.roomId)} rel="noopener" target="_blank">{sub}</a>},
|
||||
{
|
||||
a: (sub) =>
|
||||
<a href={makeRoomPermalink(this.props.roomId)} rel="noreferrer noopener" target="_blank">{sub}</a>,
|
||||
},
|
||||
);
|
||||
buttonText = _t("Invite");
|
||||
goButtonFn = this._inviteUsers;
|
||||
}
|
||||
|
||||
const hasSelection = this.state.targets.length > 0
|
||||
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||
return (
|
||||
<BaseDialog
|
||||
className='mx_InviteDialog'
|
||||
@ -1045,7 +1099,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||
kind="primary"
|
||||
onClick={goButtonFn}
|
||||
className='mx_InviteDialog_goButton'
|
||||
disabled={this.state.busy}
|
||||
disabled={this.state.busy || !hasSelection}
|
||||
>
|
||||
{buttonText}
|
||||
</AccessibleButton>
|
||||
|
@ -23,6 +23,7 @@ import VerificationRequestDialog from './VerificationRequestDialog';
|
||||
import BaseDialog from './BaseDialog';
|
||||
import DialogButtons from '../elements/DialogButtons';
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
import * as sdk from '../../../index';
|
||||
|
||||
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
|
||||
export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
@ -33,20 +34,38 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
||||
}
|
||||
|
||||
onCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog("Verification failed", "insecure", ErrorDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
title: _t("Your account is not secure"),
|
||||
description: <div>
|
||||
{_t("One of the following may be compromised:")}
|
||||
<ul>
|
||||
<li>{_t("Your password")}</li>
|
||||
<li>{_t("Your homeserver")}</li>
|
||||
<li>{_t("This session, or the other session")}</li>
|
||||
<li>{_t("The internet connection either session is using")}</li>
|
||||
</ul>
|
||||
<div>
|
||||
{_t("We recommend you change your password and recovery key in Settings immediately")}
|
||||
</div>
|
||||
</div>,
|
||||
onFinished: () => this.props.onFinished(false),
|
||||
});
|
||||
}
|
||||
|
||||
onContinueClick = async () => {
|
||||
onContinueClick = () => {
|
||||
const { userId, device } = this.props;
|
||||
const cli = MatrixClientPeg.get();
|
||||
const request = await cli.requestVerification(
|
||||
const requestPromise = cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
|
||||
this.props.onFinished(true);
|
||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||
verificationRequest: request,
|
||||
verificationRequestPromise: requestPromise,
|
||||
member: cli.getUser(userId),
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ export default createReactClass({
|
||||
focus: PropTypes.bool,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
headerImage: PropTypes.string,
|
||||
quitOnly: PropTypes.bool, // quitOnly doesn't show the cancel button just the quit [x].
|
||||
},
|
||||
|
||||
getDefaultProps: function() {
|
||||
@ -42,6 +43,7 @@ export default createReactClass({
|
||||
focus: true,
|
||||
hasCancelButton: true,
|
||||
danger: false,
|
||||
quitOnly: false,
|
||||
};
|
||||
},
|
||||
|
||||
@ -73,7 +75,7 @@ export default createReactClass({
|
||||
<DialogButtons primaryButton={this.props.button || _t('OK')}
|
||||
primaryButtonClass={primaryButtonClass}
|
||||
cancelButton={this.props.cancelButton}
|
||||
hasCancel={this.props.hasCancelButton}
|
||||
hasCancel={this.props.hasCancelButton && !this.props.quitOnly}
|
||||
onPrimaryButtonClick={this.onOk}
|
||||
focus={this.props.focus}
|
||||
onCancel={this.onCancel}
|
||||
|
@ -218,7 +218,7 @@ export default class ShareDialog extends React.Component {
|
||||
</div>
|
||||
<div className="mx_ShareDialog_social_container">
|
||||
{
|
||||
socials.map((social) => <a rel="noopener"
|
||||
socials.map((social) => <a rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
key={social.name}
|
||||
name={social.name}
|
||||
|
@ -135,7 +135,7 @@ export default class TermsDialog extends React.PureComponent {
|
||||
rows.push(<tr key={termDoc[termsLang].url}>
|
||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||
<td>{termDoc[termsLang].name} <a rel="noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
|
||||
<span className="mx_TermsDialog_link" />
|
||||
</a></td>
|
||||
<td><TermsCheckbox
|
||||
|
@ -22,7 +22,8 @@ import { _t } from '../../../languageHandler';
|
||||
|
||||
export default class VerificationRequestDialog extends React.Component {
|
||||
static propTypes = {
|
||||
verificationRequest: PropTypes.object.isRequired,
|
||||
verificationRequest: PropTypes.object,
|
||||
verificationRequestPromise: PropTypes.object,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
@ -34,6 +35,8 @@ export default class VerificationRequestDialog extends React.Component {
|
||||
render() {
|
||||
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||
const member = this.props.member ||
|
||||
MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId);
|
||||
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
|
||||
contentId="mx_Dialog_content"
|
||||
title={_t("Verification Request")}
|
||||
@ -42,14 +45,19 @@ export default class VerificationRequestDialog extends React.Component {
|
||||
<EncryptionPanel
|
||||
layout="dialog"
|
||||
verificationRequest={this.props.verificationRequest}
|
||||
verificationRequestPromise={this.props.verificationRequestPromise}
|
||||
onClose={this.props.onFinished}
|
||||
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
|
||||
member={member}
|
||||
/>
|
||||
</BaseDialog>;
|
||||
}
|
||||
|
||||
onFinished() {
|
||||
this.props.verificationRequest.cancel();
|
||||
async onFinished() {
|
||||
this.props.onFinished();
|
||||
let request = this.props.verificationRequest;
|
||||
if (!request && this.props.verificationRequestPromise) {
|
||||
request = await this.props.verificationRequestPromise;
|
||||
}
|
||||
request.cancel();
|
||||
}
|
||||
}
|
||||
|
@ -552,7 +552,7 @@ export default class AppTile extends React.Component {
|
||||
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
|
||||
Object.assign(document.createElement('a'),
|
||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
|
||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
|
||||
}
|
||||
|
||||
_onReloadWidgetClick() {
|
||||
|
@ -78,8 +78,7 @@ export class EditableItem extends React.Component {
|
||||
|
||||
return (
|
||||
<div className="mx_EditableItem">
|
||||
<img src={require("../../../../res/img/feather-customised/cancel.svg")} width={14} height={14}
|
||||
onClick={this._onRemove} className="mx_EditableItem_delete" alt={_t("Remove")} />
|
||||
<div onClick={this._onRemove} className="mx_EditableItem_delete" title={_t("Remove")} role="button" />
|
||||
<span className="mx_EditableItem_item">{this.props.value}</span>
|
||||
</div>
|
||||
);
|
||||
@ -123,8 +122,9 @@ export default class EditableItemList extends React.Component {
|
||||
<form onSubmit={this._onItemAdded} autoComplete="off"
|
||||
noValidate={true} className="mx_EditableItemList_newItem">
|
||||
<Field id={`mx_EditableItemList_new_${this.props.id}`} label={this.props.placeholder} type="text"
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary">
|
||||
autoComplete="off" value={this.props.newItem || ""} onChange={this._onNewItemChanged}
|
||||
list={this.props.suggestionsListId} />
|
||||
<AccessibleButton onClick={this._onItemAdded} kind="primary" type="submit">
|
||||
{_t("Add")}
|
||||
</AccessibleButton>
|
||||
</form>
|
||||
|
@ -32,6 +32,8 @@ export default class Field extends React.PureComponent {
|
||||
element: PropTypes.oneOf(["input", "select", "textarea"]),
|
||||
// The field's type (when used as an <input>). Defaults to "text".
|
||||
type: PropTypes.string,
|
||||
// id of a <datalist> element for suggestions
|
||||
list: PropTypes.string,
|
||||
// The field's label string.
|
||||
label: PropTypes.string,
|
||||
// The field's placeholder string. Defaults to the label.
|
||||
@ -157,7 +159,7 @@ export default class Field extends React.PureComponent {
|
||||
render() {
|
||||
const {
|
||||
element, prefix, postfix, className, onValidate, children,
|
||||
tooltipContent, flagInvalid, tooltipClassName, ...inputProps} = this.props;
|
||||
tooltipContent, flagInvalid, tooltipClassName, list, ...inputProps} = this.props;
|
||||
|
||||
const inputElement = element || "input";
|
||||
|
||||
@ -169,6 +171,7 @@ export default class Field extends React.PureComponent {
|
||||
inputProps.onFocus = this.onFocus;
|
||||
inputProps.onChange = this.onChange;
|
||||
inputProps.onBlur = this.onBlur;
|
||||
inputProps.list = list;
|
||||
|
||||
const fieldInput = React.createElement(inputElement, inputProps, children);
|
||||
|
||||
|
@ -91,7 +91,7 @@ export default class ImageView extends React.Component {
|
||||
getName() {
|
||||
let name = this.props.name;
|
||||
if (name && this.props.link) {
|
||||
name = <a href={ this.props.link } target="_blank" rel="noopener">{ name }</a>;
|
||||
name = <a href={ this.props.link } target="_blank" rel="noreferrer noopener">{ name }</a>;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ import classNames from 'classnames';
|
||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||
import PropTypes from 'prop-types';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
||||
import FlairStore from "../../../stores/FlairStore";
|
||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
@ -129,7 +128,7 @@ const Pill = createReactClass({
|
||||
const localRoom = resourceId[0] === '#' ?
|
||||
MatrixClientPeg.get().getRooms().find((r) => {
|
||||
return r.getCanonicalAlias() === resourceId ||
|
||||
r.getAliases().includes(resourceId);
|
||||
r.getAltAliases().includes(resourceId);
|
||||
}) : MatrixClientPeg.get().getRoom(resourceId);
|
||||
room = localRoom;
|
||||
if (!localRoom) {
|
||||
@ -237,12 +236,12 @@ const Pill = createReactClass({
|
||||
case Pill.TYPE_ROOM_MENTION: {
|
||||
const room = this.state.room;
|
||||
if (room) {
|
||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
||||
linkText = resource;
|
||||
if (this.props.shouldShowPillAvatar) {
|
||||
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
pillClass = 'mx_RoomPill';
|
||||
}
|
||||
break;
|
||||
case Pill.TYPE_GROUP_MENTION: {
|
||||
|
@ -92,6 +92,7 @@ export default class RoomAliasField extends React.PureComponent {
|
||||
invalid: () => _t("Please provide a room alias"),
|
||||
}, {
|
||||
key: "taken",
|
||||
final: true,
|
||||
test: async ({value}) => {
|
||||
if (!value) {
|
||||
return true;
|
||||
|
41
src/components/views/elements/SSOButton.js
Normal file
41
src/components/views/elements/SSOButton.js
Normal file
@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2020 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 React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import PlatformPeg from "../../../PlatformPeg";
|
||||
import AccessibleButton from "./AccessibleButton";
|
||||
import {_t} from "../../../languageHandler";
|
||||
|
||||
const SSOButton = ({matrixClient, loginType, ...props}) => {
|
||||
const onClick = () => {
|
||||
PlatformPeg.get().startSingleSignOn(matrixClient, loginType);
|
||||
};
|
||||
|
||||
return (
|
||||
<AccessibleButton {...props} kind="primary" onClick={onClick}>
|
||||
{_t("Sign in with single sign-on")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
};
|
||||
|
||||
SSOButton.propTypes = {
|
||||
matrixClient: PropTypes.object.isRequired, // does not use context as may use a temporary client
|
||||
loginType: PropTypes.oneOf(["sso", "cas"]), // defaults to "sso" in base-apis
|
||||
};
|
||||
|
||||
export default SSOButton;
|
@ -28,9 +28,11 @@ import classNames from 'classnames';
|
||||
* An array of rules describing how to check to input value. Each rule in an object
|
||||
* and may have the following properties:
|
||||
* - `key`: A unique ID for the rule. Required.
|
||||
* - `skip`: A function used to determine whether the rule should even be evaluated.
|
||||
* - `test`: A function used to determine the rule's current validity. Required.
|
||||
* - `valid`: Function returning text to show when the rule is valid. Only shown if set.
|
||||
* - `invalid`: Function returning text to show when the rule is invalid. Only shown if set.
|
||||
* - `final`: A Boolean if true states that this rule will only be considered if all rules before it returned valid.
|
||||
* @returns {Function}
|
||||
* A validation function that takes in the current input value and returns
|
||||
* the overall validity and a feedback UI that can be rendered for more detail.
|
||||
@ -51,9 +53,20 @@ export default function withValidation({ description, rules }) {
|
||||
if (!rule.key || !rule.test) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!valid && rule.final) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const data = { value, allowEmpty };
|
||||
|
||||
if (rule.skip && rule.skip.call(this, data)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// We're setting `this` to whichever component holds the validation
|
||||
// function. That allows rules to access the state of the component.
|
||||
const ruleValid = await rule.test.call(this, { value, allowEmpty });
|
||||
const ruleValid = await rule.test.call(this, data);
|
||||
valid = valid && ruleValid;
|
||||
if (ruleValid && rule.valid) {
|
||||
// If the rule's result is valid and has text to show for
|
||||
|
@ -17,89 +17,87 @@ limitations under the License.
|
||||
import React from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||
import * as qs from "qs";
|
||||
import QRCode from "qrcode-react";
|
||||
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
|
||||
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||
import {ToDeviceChannel} from "matrix-js-sdk/src/crypto/verification/request/ToDeviceChannel";
|
||||
import {decodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||
import Spinner from "../Spinner";
|
||||
import * as QRCode from "qrcode";
|
||||
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
|
||||
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
|
||||
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||
|
||||
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||
export default class VerificationQRCode extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// Common for all kinds of QR codes
|
||||
keys: PropTypes.array.isRequired, // array of [Key ID, Base64 Key] pairs
|
||||
action: PropTypes.string.isRequired,
|
||||
keyholderUserId: PropTypes.string.isRequired,
|
||||
|
||||
// User verification use case only
|
||||
secret: PropTypes.string,
|
||||
otherUserKey: PropTypes.string, // Base64 key being verified
|
||||
otherUserDeviceKey: PropTypes.string, // Base64 key of the other user's device (or what we think it is; optional)
|
||||
requestEventId: PropTypes.string, // for DM verification only
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
action: "verify",
|
||||
prefix: PropTypes.string.isRequired,
|
||||
version: PropTypes.number.isRequired,
|
||||
mode: PropTypes.number.isRequired,
|
||||
transactionId: PropTypes.string.isRequired, // or requestEventId
|
||||
firstKeyB64: PropTypes.string.isRequired,
|
||||
secondKeyB64: PropTypes.string.isRequired,
|
||||
secretB64: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
static async getPropsForRequest(verificationRequest: VerificationRequest) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const myUserId = cli.getUserId();
|
||||
const otherUserId = verificationRequest.otherUserId;
|
||||
const myDeviceId = cli.getDeviceId();
|
||||
const otherDevice = verificationRequest.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
|
||||
const qrProps = {
|
||||
secret: verificationRequest.encodedSharedSecret,
|
||||
keyholderUserId: myUserId,
|
||||
action: "verify",
|
||||
keys: [], // array of pairs: keyId, base64Key
|
||||
otherUserKey: "", // base64key
|
||||
otherUserDeviceKey: "", // base64key
|
||||
requestEventId: "", // we figure this out in a moment
|
||||
};
|
||||
let mode = MODE_VERIFY_OTHER_USER;
|
||||
if (myUserId === otherUserId) {
|
||||
// Mode changes depending on whether or not we trust the master cross signing key
|
||||
const myTrust = cli.checkUserTrust(myUserId);
|
||||
if (myTrust.isCrossSigningVerified()) {
|
||||
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||
} else {
|
||||
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||
}
|
||||
}
|
||||
|
||||
const requestEvent = verificationRequest.requestEvent;
|
||||
qrProps.requestEventId = requestEvent.getId()
|
||||
const transactionId = requestEvent.getId()
|
||||
? requestEvent.getId()
|
||||
: ToDeviceChannel.getTransactionId(requestEvent);
|
||||
|
||||
// Populate the keys we need depending on which direction and users are involved in the verification.
|
||||
if (myUserId === otherUserId) {
|
||||
if (!otherDeviceId) {
|
||||
// Existing scanning New session's QR code
|
||||
qrProps.otherUserDeviceKey = null;
|
||||
} else {
|
||||
// New scanning Existing session's QR code
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
const device = myDevices.find(d => d.deviceId === otherDeviceId);
|
||||
if (device) qrProps.otherUserDeviceKey = device.getFingerprint();
|
||||
}
|
||||
const qrProps = {
|
||||
prefix: BINARY_PREFIX,
|
||||
version: CODE_VERSION,
|
||||
mode,
|
||||
transactionId,
|
||||
firstKeyB64: '', // worked out shortly
|
||||
secondKeyB64: '', // worked out shortly
|
||||
secretB64: verificationRequest.encodedSharedSecret,
|
||||
};
|
||||
|
||||
// Either direction shares these next few props
|
||||
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||
|
||||
const xsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
qrProps.otherUserKey = xsignInfo.getId("master");
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
// First key is our master cross signing key
|
||||
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[xsignInfo.getId("master"), xsignInfo.getId("master")],
|
||||
];
|
||||
} else {
|
||||
// Doesn't matter which direction the verification is, we always show the same QR code
|
||||
// for not-ourself verification.
|
||||
const myXsignInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||
const otherXsignInfo = cli.getStoredCrossSigningForUser(otherUserId);
|
||||
const otherDevices = (await cli.getStoredDevicesForUser(otherUserId)) || [];
|
||||
const otherDevice = otherDevices.find(d => d.deviceId === otherDeviceId);
|
||||
// Second key is the other user's master cross signing key
|
||||
const otherUserCrossSigningInfo = cli.getStoredCrossSigningForUser(otherUserId);
|
||||
qrProps.secondKeyB64 = otherUserCrossSigningInfo.getId("master");
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
// First key is our master cross signing key
|
||||
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
|
||||
qrProps.keys = [
|
||||
[myDeviceId, cli.getDeviceEd25519Key()],
|
||||
[myXsignInfo.getId("master"), myXsignInfo.getId("master")],
|
||||
];
|
||||
qrProps.otherUserKey = otherXsignInfo.getId("master");
|
||||
if (otherDevice) qrProps.otherUserDeviceKey = otherDevice.getFingerprint();
|
||||
// Second key is the other device's device key
|
||||
const otherDevice = verificationRequest.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
const device = myDevices.find(d => d.deviceId === otherDeviceId);
|
||||
qrProps.secondKeyB64 = device.getFingerprint();
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
// First key is our device's key
|
||||
qrProps.firstKeyB64 = cli.getDeviceEd25519Key();
|
||||
|
||||
// Second key is what we think our master cross signing key is
|
||||
qrProps.secondKeyB64 = myCrossSigningInfo.getId("master");
|
||||
}
|
||||
|
||||
return qrProps;
|
||||
@ -107,21 +105,63 @@ export default class VerificationQRCode extends React.PureComponent {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
dataUri: null,
|
||||
};
|
||||
this.generateQrCode();
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps): void {
|
||||
if (JSON.stringify(this.props) === JSON.stringify(prevProps)) return; // No prop change
|
||||
|
||||
this.generateQRCode();
|
||||
}
|
||||
|
||||
async generateQrCode() {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b: number) => {
|
||||
const tmpBuf = Buffer.from([b]);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendInt = (i: number) => {
|
||||
const tmpBuf = Buffer.alloc(2);
|
||||
tmpBuf.writeInt16BE(i, 0);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendStr = (s: string, enc: string, withLengthPrefix = true) => {
|
||||
const tmpBuf = Buffer.from(s, enc);
|
||||
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendEncBase64 = (b64: string) => {
|
||||
const b = decodeBase64(b64);
|
||||
const tmpBuf = Buffer.from(b);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
|
||||
// Actually build the buffer for the QR code
|
||||
appendStr(this.props.prefix, "ascii", false);
|
||||
appendByte(this.props.version);
|
||||
appendByte(this.props.mode);
|
||||
appendStr(this.props.transactionId, "utf-8");
|
||||
appendEncBase64(this.props.firstKeyB64);
|
||||
appendEncBase64(this.props.secondKeyB64);
|
||||
appendEncBase64(this.props.secretB64);
|
||||
|
||||
// Now actually assemble the QR code's data URI
|
||||
const uri = await QRCode.toDataURL([{data: buf, mode: 'byte'}], {
|
||||
errorCorrectionLevel: 'L', // we want it as trivial-looking as possible
|
||||
});
|
||||
this.setState({dataUri: uri});
|
||||
}
|
||||
|
||||
render() {
|
||||
const query = {
|
||||
request: this.props.requestEventId,
|
||||
action: this.props.action,
|
||||
other_user_key: this.props.otherUserKey,
|
||||
secret: this.props.secret,
|
||||
};
|
||||
for (const key of this.props.keys) {
|
||||
query[`key_${key[0]}`] = key[1];
|
||||
if (!this.state.dataUri) {
|
||||
return <div className='mx_VerificationQRCode'><Spinner /></div>;
|
||||
}
|
||||
|
||||
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
|
||||
|
||||
return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
|
||||
return <img src={this.state.dataUri} className='mx_VerificationQRCode' />;
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ export default createReactClass({
|
||||
|
||||
const av = (
|
||||
<BaseAvatar
|
||||
aria-hidden="true"
|
||||
name={this.props.member.displayname || this.props.member.userId}
|
||||
idName={this.props.member.userId}
|
||||
width={36} height={36}
|
||||
|
@ -20,7 +20,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||
import {formatTime} from '../../../DateUtils';
|
||||
import {MatrixEvent} from 'matrix-js-sdk';
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||
@ -53,6 +53,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
||||
|
||||
this._content = createRef();
|
||||
this._pills = [];
|
||||
}
|
||||
|
||||
_onAssociatedStatusChanged = () => {
|
||||
@ -81,7 +82,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||
pillifyLinks() {
|
||||
// not present for redacted events
|
||||
if (this._content.current) {
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent);
|
||||
pillifyLinks(this._content.current.children, this.props.mxEvent, this._pills);
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,6 +91,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
unmountPills(this._pills);
|
||||
const event = this.props.mxEvent;
|
||||
if (event.localRedactionEvent()) {
|
||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||
|
@ -247,6 +247,8 @@ export default createReactClass({
|
||||
});
|
||||
};
|
||||
|
||||
// This button should actually Download because usercontent/ will try to click itself
|
||||
// but it is not guaranteed between various browsers' settings.
|
||||
return (
|
||||
<span className="mx_MFileBody">
|
||||
<div className="mx_MFileBody_download">
|
||||
@ -269,6 +271,8 @@ export default createReactClass({
|
||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||
download: fileName,
|
||||
textContent: _t("Download %(text)s", { text: text }),
|
||||
// only auto-download if a user triggered this iframe explicitly
|
||||
auto: !this.props.decryptedBlob,
|
||||
}, "*");
|
||||
};
|
||||
|
||||
@ -290,14 +294,14 @@ export default createReactClass({
|
||||
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||
onLoad={onIframeLoad}
|
||||
ref={this._iframe}
|
||||
sandbox="allow-scripts allow-downloads" />
|
||||
sandbox="allow-scripts allow-downloads allow-downloads-without-user-activation" />
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
} else if (contentUrl) {
|
||||
const downloadProps = {
|
||||
target: "_blank",
|
||||
rel: "noopener",
|
||||
rel: "noreferrer noopener",
|
||||
|
||||
// We set the href regardless of whether or not we intercept the download
|
||||
// because we don't really want to convert the file to a blob eagerly, and
|
||||
|
@ -62,8 +62,8 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||
const request = this.props.mxEvent.verificationRequest;
|
||||
if (request) {
|
||||
try {
|
||||
await request.accept();
|
||||
this._openRequest();
|
||||
await request.accept();
|
||||
} catch (err) {
|
||||
console.error(err.message);
|
||||
}
|
||||
@ -136,9 +136,9 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||
} else if (request.cancelled) {
|
||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||
} else if (request.accepting) {
|
||||
stateLabel = _t("accepting …");
|
||||
stateLabel = _t("Accepting …");
|
||||
} else if (request.declining) {
|
||||
stateLabel = _t("declining …");
|
||||
stateLabel = _t("Declining …");
|
||||
}
|
||||
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
|
||||
}
|
||||
|
@ -121,10 +121,12 @@ export default class MessageActionBar extends React.PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.mxEvent.on("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.on("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.mxEvent.removeListener("Event.decrypted", this.onDecrypted);
|
||||
this.props.mxEvent.removeListener("Event.beforeRedaction", this.onBeforeRedaction);
|
||||
}
|
||||
|
||||
onDecrypted = () => {
|
||||
@ -133,6 +135,11 @@ export default class MessageActionBar extends React.PureComponent {
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onBeforeRedaction = () => {
|
||||
// When an event is redacted, we can't edit it so update the available actions.
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
onFocusChange = (focused) => {
|
||||
if (!this.props.onFocusChange) {
|
||||
return;
|
||||
|
@ -30,7 +30,7 @@ import { _t } from '../../../languageHandler';
|
||||
import * as ContextMenu from '../../structures/ContextMenu';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import ReplyThread from "../elements/ReplyThread";
|
||||
import {pillifyLinks} from '../../../utils/pillify';
|
||||
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||
import {toRightOf} from "../../structures/ContextMenu";
|
||||
@ -92,6 +92,7 @@ export default createReactClass({
|
||||
|
||||
componentDidMount: function() {
|
||||
this._unmounted = false;
|
||||
this._pills = [];
|
||||
if (!this.props.editState) {
|
||||
this._applyFormatting();
|
||||
}
|
||||
@ -103,7 +104,7 @@ export default createReactClass({
|
||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
||||
// are still sent as plaintext URLs. If these are ever pillified in the composer,
|
||||
// we should be pillify them here by doing the linkifying BEFORE the pillifying.
|
||||
pillifyLinks([this._content.current], this.props.mxEvent);
|
||||
pillifyLinks([this._content.current], this.props.mxEvent, this._pills);
|
||||
HtmlUtils.linkifyElement(this._content.current);
|
||||
this.calculateUrlPreview();
|
||||
|
||||
@ -146,6 +147,7 @@ export default createReactClass({
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this._unmounted = true;
|
||||
unmountPills(this._pills);
|
||||
},
|
||||
|
||||
shouldComponentUpdate: function(nextProps, nextState) {
|
||||
@ -372,7 +374,9 @@ export default createReactClass({
|
||||
const height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||
const left = (window.screen.width - width) / 2;
|
||||
const top = (window.screen.height - height) / 2;
|
||||
window.open(completeUrl, '_blank', `height=${height}, width=${width}, top=${top}, left=${left},`);
|
||||
const features = `height=${height}, width=${width}, top=${top}, left=${left},`;
|
||||
const wnd = window.open(completeUrl, '_blank', features);
|
||||
wnd.opener = null;
|
||||
},
|
||||
});
|
||||
});
|
||||
|
@ -28,12 +28,17 @@ export const PendingActionSpinner = ({text}) => {
|
||||
</div>;
|
||||
};
|
||||
|
||||
const EncryptionInfo = ({pending, member, onStartVerification}) => {
|
||||
const EncryptionInfo = ({waitingForOtherParty, waitingForNetwork, member, onStartVerification}) => {
|
||||
let content;
|
||||
if (pending) {
|
||||
const text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
if (waitingForOtherParty || waitingForNetwork) {
|
||||
let text;
|
||||
if (waitingForOtherParty) {
|
||||
text = _t("Waiting for %(displayName)s to accept…", {
|
||||
displayName: member.displayName || member.name || member.userId,
|
||||
});
|
||||
} else {
|
||||
text = _t("Accepting…");
|
||||
}
|
||||
content = <PendingActionSpinner text={text} />;
|
||||
} else {
|
||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||
|
@ -30,13 +30,32 @@ import {_t} from "../../../languageHandler";
|
||||
// cancellation codes which constitute a key mismatch
|
||||
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
||||
|
||||
const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||
const EncryptionPanel = ({verificationRequest, verificationRequestPromise, member, onClose, layout}) => {
|
||||
const [request, setRequest] = useState(verificationRequest);
|
||||
// state to show a spinner immediately after clicking "start verification",
|
||||
// before we have a request
|
||||
const [isRequesting, setRequesting] = useState(false);
|
||||
const [phase, setPhase] = useState(request && request.phase);
|
||||
useEffect(() => {
|
||||
setRequest(verificationRequest);
|
||||
if (verificationRequest) {
|
||||
setRequesting(false);
|
||||
setPhase(verificationRequest.phase);
|
||||
}
|
||||
}, [verificationRequest]);
|
||||
|
||||
const [phase, setPhase] = useState(request && request.phase);
|
||||
useEffect(() => {
|
||||
async function awaitPromise() {
|
||||
setRequesting(true);
|
||||
const request = await verificationRequestPromise;
|
||||
setRequesting(false);
|
||||
setRequest(request);
|
||||
setPhase(request.phase);
|
||||
}
|
||||
if (verificationRequestPromise) {
|
||||
awaitPromise();
|
||||
}
|
||||
}, [verificationRequestPromise]);
|
||||
const changeHandler = useCallback(() => {
|
||||
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
|
||||
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
|
||||
@ -65,6 +84,7 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||
useEventEmitter(request, "change", changeHandler);
|
||||
|
||||
const onStartVerification = useCallback(async () => {
|
||||
setRequesting(true);
|
||||
const cli = MatrixClientPeg.get();
|
||||
const roomId = await ensureDMExists(cli, member.userId);
|
||||
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
|
||||
@ -72,9 +92,16 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||
setPhase(verificationRequest.phase);
|
||||
}, [member.userId]);
|
||||
|
||||
const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
|
||||
const requested =
|
||||
(!request && isRequesting) ||
|
||||
(request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined));
|
||||
if (!request || requested) {
|
||||
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
|
||||
const initiatedByMe = (!request && isRequesting) || (request && request.initiatedByMe);
|
||||
return <EncryptionInfo
|
||||
onStartVerification={onStartVerification}
|
||||
member={member}
|
||||
waitingForOtherParty={requested && initiatedByMe}
|
||||
waitingForNetwork={requested && !initiatedByMe} />;
|
||||
} else {
|
||||
return (
|
||||
<VerificationPanel
|
||||
|
@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
|
||||
import Modal from '../../../Modal';
|
||||
import * as sdk from '../../../index';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import createRoom, {findDMForUser} from '../../../createRoom';
|
||||
import createRoom from '../../../createRoom';
|
||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||
import AccessibleButton from '../elements/AccessibleButton';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
@ -42,6 +42,8 @@ import {textualPowerLevel} from '../../../Roles';
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||
import EncryptionPanel from "./EncryptionPanel";
|
||||
import { useAsyncMemo } from '../../../hooks/useAsyncMemo';
|
||||
import { verifyUser, legacyVerifyUser, verifyDevice } from '../../../verification';
|
||||
|
||||
const _disambiguateDevices = (devices) => {
|
||||
const names = Object.create(null);
|
||||
@ -135,54 +137,21 @@ function useIsEncrypted(cli, room) {
|
||||
return isEncrypted;
|
||||
}
|
||||
|
||||
async function verifyDevice(userId, device) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const member = cli.getUser(userId);
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
Modal.createTrackedDialog("Verification warning", "unverified session", QuestionDialog, {
|
||||
headerImage: require("../../../../res/img/e2e/warning.svg"),
|
||||
title: _t("Not Trusted"),
|
||||
description: <div>
|
||||
<p>{_t("%(name)s (%(userId)s) signed in to a new session without verifying it:", {name: member.displayName, userId})}</p>
|
||||
<p>{device.getDisplayName()} ({device.deviceId})</p>
|
||||
<p>{_t("Ask this user to verify their session, or manually verify it below.")}</p>
|
||||
</div>,
|
||||
onFinished: async (doneClicked) => {
|
||||
const manuallyVerifyClicked = !doneClicked;
|
||||
if (!manuallyVerifyClicked) {
|
||||
return;
|
||||
}
|
||||
const cli = MatrixClientPeg.get();
|
||||
const verificationRequest = await cli.requestVerification(
|
||||
userId,
|
||||
[device.deviceId],
|
||||
);
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {member, verificationRequest},
|
||||
});
|
||||
},
|
||||
primaryButton: _t("Done"),
|
||||
cancelButton: _t("Manually Verify"),
|
||||
});
|
||||
}
|
||||
|
||||
function verifyUser(user) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const dmRoom = findDMForUser(cli, user.userId);
|
||||
let existingRequest;
|
||||
if (dmRoom) {
|
||||
existingRequest = cli.findVerificationRequestDMInProgress(dmRoom.roomId);
|
||||
}
|
||||
dis.dispatch({
|
||||
action: "set_right_panel_phase",
|
||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||
refireParams: {
|
||||
member: user,
|
||||
verificationRequest: existingRequest,
|
||||
},
|
||||
});
|
||||
function useHasCrossSigningKeys(cli, member, canVerify, setUpdating) {
|
||||
return useAsyncMemo(async () => {
|
||||
if (!canVerify) {
|
||||
return false;
|
||||
}
|
||||
setUpdating(true);
|
||||
try {
|
||||
await cli.downloadKeys([member.userId]);
|
||||
const xsi = cli.getStoredCrossSigningForUser(member.userId);
|
||||
const key = xsi && xsi.getId();
|
||||
return !!key;
|
||||
} finally {
|
||||
setUpdating(false);
|
||||
}
|
||||
}, [cli, member, canVerify], false);
|
||||
}
|
||||
|
||||
function DeviceItem({userId, device}) {
|
||||
@ -211,7 +180,7 @@ function DeviceItem({userId, device}) {
|
||||
|
||||
const onDeviceClick = () => {
|
||||
if (!isVerified) {
|
||||
verifyDevice(userId, device);
|
||||
verifyDevice(cli.getUser(userId), device);
|
||||
}
|
||||
};
|
||||
|
||||
@ -916,6 +885,12 @@ const useIsSynapseAdmin = (cli) => {
|
||||
return isAdmin;
|
||||
};
|
||||
|
||||
const useHomeserverSupportsCrossSigning = (cli) => {
|
||||
return useAsyncMemo(async () => {
|
||||
return cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing");
|
||||
}, [cli], false);
|
||||
};
|
||||
|
||||
function useRoomPermissions(cli, room, user) {
|
||||
const [roomPermissions, setRoomPermissions] = useState({
|
||||
// modifyLevelMax is the max PL we can set this user to, typically min(their PL, our PL) && canSetPL
|
||||
@ -1315,16 +1290,31 @@ const BasicUserInfo = ({room, member, groupId, devices, isRoomEncrypted}) => {
|
||||
text = _t("Messages in this room are end-to-end encrypted.");
|
||||
}
|
||||
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = SettingsStore.isFeatureEnabled("feature_cross_signing") ?
|
||||
userTrust.isCrossSigningVerified() :
|
||||
userTrust.isVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
|
||||
let verifyButton;
|
||||
if (isRoomEncrypted && !userVerified && !isMe) {
|
||||
const homeserverSupportsCrossSigning = useHomeserverSupportsCrossSigning(cli);
|
||||
|
||||
const userTrust = cli.checkUserTrust(member.userId);
|
||||
const userVerified = userTrust.isCrossSigningVerified();
|
||||
const isMe = member.userId === cli.getUserId();
|
||||
const canVerify = SettingsStore.isFeatureEnabled("feature_cross_signing") &&
|
||||
homeserverSupportsCrossSigning &&
|
||||
isRoomEncrypted && !userVerified && !isMe;
|
||||
|
||||
const setUpdating = (updating) => {
|
||||
setPendingUpdateCount(count => count + (updating ? 1 : -1));
|
||||
};
|
||||
const hasCrossSigningKeys =
|
||||
useHasCrossSigningKeys(cli, member, canVerify, setUpdating );
|
||||
|
||||
if (canVerify) {
|
||||
verifyButton = (
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => verifyUser(member)}>
|
||||
<AccessibleButton className="mx_UserInfo_field" onClick={() => {
|
||||
if (hasCrossSigningKeys) {
|
||||
verifyUser(member);
|
||||
} else {
|
||||
legacyVerifyUser(member);
|
||||
}
|
||||
}}>
|
||||
{_t("Verify")}
|
||||
</AccessibleButton>
|
||||
);
|
||||
|
@ -270,6 +270,8 @@ export default class VerificationPanel extends React.PureComponent {
|
||||
};
|
||||
|
||||
_onVerifierShowSas = (sasEvent) => {
|
||||
const {request} = this.props;
|
||||
request.verifier.off('show_sas', this._onVerifierShowSas);
|
||||
this.setState({sasEvent});
|
||||
};
|
||||
|
||||
@ -278,7 +280,7 @@ export default class VerificationPanel extends React.PureComponent {
|
||||
const hadVerifier = this._hasVerifier;
|
||||
this._hasVerifier = !!request.verifier;
|
||||
if (!hadVerifier && this._hasVerifier) {
|
||||
request.verifier.once('show_sas', this._onVerifierShowSas);
|
||||
request.verifier.on('show_sas', this._onVerifierShowSas);
|
||||
try {
|
||||
// on the requester side, this is also awaited in _startSAS,
|
||||
// but that's ok as verify should return the same promise.
|
||||
@ -299,6 +301,10 @@ export default class VerificationPanel extends React.PureComponent {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.request.off("change", this._onRequestChange);
|
||||
const {request} = this.props;
|
||||
if (request.verifier) {
|
||||
request.verifier.off('show_sas', this._onVerifierShowSas);
|
||||
}
|
||||
request.off("change", this._onRequestChange);
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import Field from "../elements/Field";
|
||||
import ErrorDialog from "../dialogs/ErrorDialog";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Modal from "../../../Modal";
|
||||
import RoomPublishSetting from "./RoomPublishSetting";
|
||||
|
||||
class EditableAliasesList extends EditableItemList {
|
||||
constructor(props) {
|
||||
@ -46,6 +47,11 @@ class EditableAliasesList extends EditableItemList {
|
||||
};
|
||||
|
||||
_renderNewItemField() {
|
||||
// if we don't need the RoomAliasField,
|
||||
// we don't need to overriden version of _renderNewItemField
|
||||
if (!this.props.domain) {
|
||||
return super._renderNewItemField();
|
||||
}
|
||||
const RoomAliasField = sdk.getComponent('views.elements.RoomAliasField');
|
||||
const onChange = (alias) => this._onNewItemChanged({target: {value: alias}});
|
||||
return (
|
||||
@ -87,91 +93,64 @@ export default class AliasSettings extends React.Component {
|
||||
super(props);
|
||||
|
||||
const state = {
|
||||
domainToAliases: {}, // { domain.com => [#alias1:domain.com, #alias2:domain.com] }
|
||||
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||
canonicalAlias: null, // #canonical:domain.com
|
||||
altAliases: [], // [ #alias:domain.tld, ... ]
|
||||
localAliases: [], // [ #alias:my-hs.tld, ... ]
|
||||
canonicalAlias: null, // #canonical:domain.tld
|
||||
updatingCanonicalAlias: false,
|
||||
localAliasesLoading: true,
|
||||
localAliasesLoading: false,
|
||||
detailsOpen: false,
|
||||
};
|
||||
|
||||
if (props.canonicalAliasEvent) {
|
||||
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
|
||||
const content = props.canonicalAliasEvent.getContent();
|
||||
const altAliases = content.alt_aliases;
|
||||
if (Array.isArray(altAliases)) {
|
||||
state.altAliases = altAliases.slice();
|
||||
}
|
||||
state.canonicalAlias = content.alias;
|
||||
}
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
componentDidMount() {
|
||||
if (this.props.canSetCanonicalAlias) {
|
||||
// load local aliases for providing recommendations
|
||||
// for the canonical alias and alt_aliases
|
||||
this.loadLocalAliases();
|
||||
}
|
||||
}
|
||||
|
||||
async loadLocalAliases() {
|
||||
this.setState({ localAliasesLoading: true });
|
||||
try {
|
||||
const cli = MatrixClientPeg.get();
|
||||
let localAliases = [];
|
||||
if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) {
|
||||
const response = await cli.unstableGetLocalAliases(this.props.roomId);
|
||||
const localAliases = response.aliases;
|
||||
const localDomain = cli.getDomain();
|
||||
const domainToAliases = Object.assign(
|
||||
{},
|
||||
// FIXME, any localhost alt_aliases will be ignored as they are overwritten by localAliases
|
||||
this.aliasesToDictionary(this._getAltAliases()),
|
||||
{[localDomain]: localAliases || []},
|
||||
);
|
||||
const remoteDomains = Object.keys(domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && domainToAliases[domain].length > 0;
|
||||
});
|
||||
this.setState({ domainToAliases, remoteDomains });
|
||||
} else {
|
||||
const state = {};
|
||||
const localDomain = cli.getDomain();
|
||||
state.domainToAliases = this.aliasEventsToDictionary(this.props.aliasEvents || []);
|
||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
|
||||
return domain !== localDomain && state.domainToAliases[domain].length > 0;
|
||||
});
|
||||
this.setState(state);
|
||||
if (Array.isArray(response.aliases)) {
|
||||
localAliases = response.aliases;
|
||||
}
|
||||
}
|
||||
this.setState({ localAliases });
|
||||
} finally {
|
||||
this.setState({localAliasesLoading: false});
|
||||
this.setState({ localAliasesLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
aliasesToDictionary(aliases) {
|
||||
return aliases.reduce((dict, alias) => {
|
||||
const domain = alias.split(":")[1];
|
||||
dict[domain] = dict[domain] || [];
|
||||
dict[domain].push(alias);
|
||||
return dict;
|
||||
}, {});
|
||||
}
|
||||
|
||||
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
|
||||
const dict = {};
|
||||
aliasEvents.forEach((event) => {
|
||||
dict[event.getStateKey()] = (
|
||||
(event.getContent().aliases || []).slice() // shallow-copy
|
||||
);
|
||||
});
|
||||
return dict;
|
||||
}
|
||||
|
||||
_getAltAliases() {
|
||||
if (this.props.canonicalAliasEvent) {
|
||||
const altAliases = this.props.canonicalAliasEvent.getContent().alt_aliases;
|
||||
if (Array.isArray(altAliases)) {
|
||||
return altAliases;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
changeCanonicalAlias(alias) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
const oldAlias = this.state.canonicalAlias;
|
||||
this.setState({
|
||||
canonicalAlias: alias,
|
||||
updatingCanonicalAlias: true,
|
||||
});
|
||||
|
||||
const eventContent = {};
|
||||
const altAliases = this._getAltAliases();
|
||||
if (altAliases) eventContent["alt_aliases"] = altAliases;
|
||||
const eventContent = {
|
||||
alt_aliases: this.state.altAliases,
|
||||
};
|
||||
|
||||
if (alias) eventContent["alias"] = alias;
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
||||
@ -184,6 +163,39 @@ export default class AliasSettings extends React.Component {
|
||||
"or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
this.setState({canonicalAlias: oldAlias});
|
||||
}).finally(() => {
|
||||
this.setState({updatingCanonicalAlias: false});
|
||||
});
|
||||
}
|
||||
|
||||
changeAltAliases(altAliases) {
|
||||
if (!this.props.canSetCanonicalAlias) return;
|
||||
|
||||
this.setState({
|
||||
updatingCanonicalAlias: true,
|
||||
altAliases,
|
||||
});
|
||||
|
||||
const eventContent = {};
|
||||
|
||||
if (this.state.canonicalAlias) {
|
||||
eventContent.alias = this.state.canonicalAlias;
|
||||
}
|
||||
if (altAliases) {
|
||||
eventContent["alt_aliases"] = altAliases;
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
||||
eventContent, "").catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error updating alternative addresses', '', ErrorDialog, {
|
||||
title: _t("Error updating main address"),
|
||||
description: _t(
|
||||
"There was an error updating the room's alternative addresses. " +
|
||||
"It may not be allowed by the server or a temporary failure occurred.",
|
||||
),
|
||||
});
|
||||
}).finally(() => {
|
||||
this.setState({updatingCanonicalAlias: false});
|
||||
});
|
||||
@ -200,16 +212,10 @@ export default class AliasSettings extends React.Component {
|
||||
if (!alias.includes(':')) alias += ':' + localDomain;
|
||||
|
||||
MatrixClientPeg.get().createAlias(alias, this.props.roomId).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain] || [];
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = [...localAliases, alias];
|
||||
|
||||
this.setState({
|
||||
domainToAliases: domainAliases,
|
||||
// Reset the add field
|
||||
newAlias: "",
|
||||
localAliases: this.state.localAliases.concat(alias),
|
||||
newAlias: null,
|
||||
});
|
||||
|
||||
if (!this.state.canonicalAlias) {
|
||||
this.changeCanonicalAlias(alias);
|
||||
}
|
||||
@ -226,38 +232,78 @@ export default class AliasSettings extends React.Component {
|
||||
};
|
||||
|
||||
onLocalAliasDeleted = (index) => {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
const alias = this.state.domainToAliases[localDomain][index];
|
||||
|
||||
const alias = this.state.localAliases[index];
|
||||
// TODO: In future, we should probably be making sure that the alias actually belongs
|
||||
// to this room. See https://github.com/vector-im/riot-web/issues/7353
|
||||
MatrixClientPeg.get().deleteAlias(alias).then(() => {
|
||||
const localAliases = this.state.domainToAliases[localDomain].filter((a) => a !== alias);
|
||||
const domainAliases = Object.assign({}, this.state.domainToAliases);
|
||||
domainAliases[localDomain] = localAliases;
|
||||
|
||||
this.setState({domainToAliases: domainAliases});
|
||||
const localAliases = this.state.localAliases.slice();
|
||||
localAliases.splice(index, 1);
|
||||
this.setState({localAliases});
|
||||
|
||||
if (this.state.canonicalAlias === alias) {
|
||||
this.changeCanonicalAlias(null);
|
||||
}
|
||||
}).catch((err) => {
|
||||
console.error(err);
|
||||
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
|
||||
title: _t("Error removing alias"),
|
||||
description: _t(
|
||||
let description;
|
||||
if (err.errcode === "M_FORBIDDEN") {
|
||||
description = _t("You don't have permission to delete the alias.");
|
||||
} else {
|
||||
description = _t(
|
||||
"There was an error removing that alias. It may no longer exist or a temporary " +
|
||||
"error occurred.",
|
||||
),
|
||||
);
|
||||
}
|
||||
Modal.createTrackedDialog('Error removing alias', '', ErrorDialog, {
|
||||
title: _t("Error removing alias"),
|
||||
description,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
onLocalAliasesToggled = (event) => {
|
||||
// expanded
|
||||
if (event.target.open) {
|
||||
// if local aliases haven't been preloaded yet at component mount
|
||||
if (!this.props.canSetCanonicalAlias && this.state.localAliases.length === 0) {
|
||||
this.loadLocalAliases();
|
||||
}
|
||||
}
|
||||
this.setState({detailsOpen: event.target.open});
|
||||
};
|
||||
|
||||
onCanonicalAliasChange = (event) => {
|
||||
this.changeCanonicalAlias(event.target.value);
|
||||
};
|
||||
|
||||
onNewAltAliasChanged = (value) => {
|
||||
this.setState({newAltAlias: value});
|
||||
}
|
||||
|
||||
onAltAliasAdded = (alias) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
if (!altAliases.some(a => a.trim() === alias.trim())) {
|
||||
altAliases.push(alias.trim());
|
||||
this.changeAltAliases(altAliases);
|
||||
this.setState({newAltAlias: ""});
|
||||
}
|
||||
}
|
||||
|
||||
onAltAliasDeleted = (index) => {
|
||||
const altAliases = this.state.altAliases.slice();
|
||||
altAliases.splice(index, 1);
|
||||
this.changeAltAliases(altAliases);
|
||||
}
|
||||
|
||||
_getAliases() {
|
||||
return this.state.altAliases.concat(this._getLocalNonAltAliases());
|
||||
}
|
||||
|
||||
_getLocalNonAltAliases() {
|
||||
const {altAliases} = this.state;
|
||||
return this.state.localAliases.filter(alias => !altAliases.includes(alias));
|
||||
}
|
||||
|
||||
render() {
|
||||
const localDomain = MatrixClientPeg.get().getDomain();
|
||||
|
||||
@ -269,15 +315,13 @@ export default class AliasSettings extends React.Component {
|
||||
element='select' id='canonicalAlias' label={_t('Main address')}>
|
||||
<option value="" key="unset">{ _t('not specified') }</option>
|
||||
{
|
||||
Object.keys(this.state.domainToAliases).map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i + "_" + j}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
});
|
||||
this._getAliases().map((alias, i) => {
|
||||
if (alias === this.state.canonicalAlias) found = true;
|
||||
return (
|
||||
<option value={alias} key={i}>
|
||||
{ alias }
|
||||
</option>
|
||||
);
|
||||
})
|
||||
}
|
||||
{
|
||||
@ -289,53 +333,60 @@ export default class AliasSettings extends React.Component {
|
||||
</Field>
|
||||
);
|
||||
|
||||
let remoteAliasesSection;
|
||||
if (this.state.remoteDomains.length) {
|
||||
remoteAliasesSection = (
|
||||
<div>
|
||||
<div>
|
||||
{ _t("Remote addresses for this room:") }
|
||||
</div>
|
||||
<ul>
|
||||
{ this.state.remoteDomains.map((domain, i) => {
|
||||
return this.state.domainToAliases[domain].map((alias, j) => {
|
||||
return <li key={i + "_" + j}>{alias}</li>;
|
||||
});
|
||||
}) }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let localAliasesList;
|
||||
if (this.state.localAliasesLoading) {
|
||||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
localAliasesList = <Spinner />;
|
||||
} else {
|
||||
localAliasesList = <EditableAliasesList
|
||||
localAliasesList = (<EditableAliasesList
|
||||
id="roomAliases"
|
||||
className={"mx_RoomSettings_localAliases"}
|
||||
items={this.state.domainToAliases[localDomain] || []}
|
||||
items={this.state.localAliases}
|
||||
newItem={this.state.newAlias}
|
||||
onNewItemChanged={this.onNewAliasChanged}
|
||||
canRemove={this.props.canSetAliases}
|
||||
canEdit={this.props.canSetAliases}
|
||||
onItemAdded={this.onLocalAliasAdded}
|
||||
onItemRemoved={this.onLocalAliasDeleted}
|
||||
itemsLabel={_t('Local addresses for this room:')}
|
||||
noItemsLabel={_t('This room has no local addresses')}
|
||||
placeholder={_t(
|
||||
'New address (e.g. #foo:%(localDomain)s)', {localDomain: localDomain},
|
||||
)}
|
||||
placeholder={_t('Local address')}
|
||||
domain={localDomain}
|
||||
/>;
|
||||
/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='mx_AliasSettings'>
|
||||
<span className='mx_SettingsTab_subheading'>{_t("Published Addresses")}</span>
|
||||
<p>{_t("Published addresses can be used by anyone on any server to join your room. " +
|
||||
"To publish an address, it needs to be set as a local address first.")}</p>
|
||||
{canonicalAliasSection}
|
||||
{localAliasesList}
|
||||
{remoteAliasesSection}
|
||||
<RoomPublishSetting roomId={this.props.roomId} canSetCanonicalAlias={this.props.canSetCanonicalAlias} />
|
||||
<datalist id="mx_AliasSettings_altRecommendations">
|
||||
{this._getLocalNonAltAliases().map(alias => {
|
||||
return <option value={alias} key={alias} />;
|
||||
})};
|
||||
</datalist>
|
||||
<EditableAliasesList
|
||||
id="roomAltAliases"
|
||||
className={"mx_RoomSettings_altAliases"}
|
||||
items={this.state.altAliases}
|
||||
newItem={this.state.newAltAlias}
|
||||
onNewItemChanged={this.onNewAltAliasChanged}
|
||||
canRemove={this.props.canSetCanonicalAlias}
|
||||
canEdit={this.props.canSetCanonicalAlias}
|
||||
onItemAdded={this.onAltAliasAdded}
|
||||
onItemRemoved={this.onAltAliasDeleted}
|
||||
suggestionsListId="mx_AliasSettings_altRecommendations"
|
||||
itemsLabel={_t('Other published addresses:')}
|
||||
noItemsLabel={_t('No other published addresses yet, add one below')}
|
||||
placeholder={_t('New published address (e.g. #alias:server)')}
|
||||
/>
|
||||
<span className='mx_SettingsTab_subheading mx_AliasSettings_localAliasHeader'>{_t("Local Addresses")}</span>
|
||||
<p>{_t("Set addresses for this room so users can find this room through your homeserver (%(localDomain)s)", {localDomain})}</p>
|
||||
<details onToggle={this.onLocalAliasesToggled}>
|
||||
<summary>{ this.state.detailsOpen ? _t('Show less') : _t("Show more")}</summary>
|
||||
{localAliasesList}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
60
src/components/views/room_settings/RoomPublishSetting.js
Normal file
60
src/components/views/room_settings/RoomPublishSetting.js
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2020 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 React from 'react';
|
||||
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
|
||||
import {_t} from "../../../languageHandler";
|
||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||
|
||||
export default class RoomPublishSetting extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {isRoomPublished: false};
|
||||
}
|
||||
|
||||
onRoomPublishChange = (e) => {
|
||||
const valueBefore = this.state.isRoomPublished;
|
||||
const newValue = !valueBefore;
|
||||
this.setState({isRoomPublished: newValue});
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
client.setRoomDirectoryVisibility(
|
||||
this.props.roomId,
|
||||
newValue ? 'public' : 'private',
|
||||
).catch(() => {
|
||||
// Roll back the local echo on the change
|
||||
this.setState({isRoomPublished: valueBefore});
|
||||
});
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const client = MatrixClientPeg.get();
|
||||
client.getRoomDirectoryVisibility(this.props.roomId).then((result => {
|
||||
this.setState({isRoomPublished: result.visibility === 'public'});
|
||||
}));
|
||||
}
|
||||
|
||||
render() {
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
return (<LabelledToggleSwitch value={this.state.isRoomPublished}
|
||||
onChange={this.onRoomPublishChange}
|
||||
disabled={!this.props.canSetCanonicalAlias}
|
||||
label={_t("Publish this room to the public in %(domain)s's room directory?", {
|
||||
domain: client.getDomain(),
|
||||
})} />);
|
||||
}
|
||||
}
|
@ -219,7 +219,7 @@ export default createReactClass({
|
||||
|
||||
if (link) {
|
||||
span = (
|
||||
<a href={link} target="_blank" rel="noopener">
|
||||
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||
{ span }
|
||||
</a>
|
||||
);
|
||||
|
@ -393,6 +393,20 @@ export default class BasicMessageEditor extends React.Component {
|
||||
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
|
||||
this._insertText("\n");
|
||||
handled = true;
|
||||
// move selection to start of composer
|
||||
} else if (modKey && event.key === Key.HOME && !event.shiftKey) {
|
||||
setSelection(this._editorRef, model, {
|
||||
index: 0,
|
||||
offset: 0,
|
||||
});
|
||||
handled = true;
|
||||
// move selection to end of composer
|
||||
} else if (modKey && event.key === Key.END && !event.shiftKey) {
|
||||
setSelection(this._editorRef, model, {
|
||||
index: model.parts.length - 1,
|
||||
offset: model.parts[model.parts.length - 1].text.length,
|
||||
});
|
||||
handled = true;
|
||||
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||
} else {
|
||||
const metaOrAltPressed = event.metaKey || event.altKey;
|
||||
@ -458,10 +472,14 @@ export default class BasicMessageEditor extends React.Component {
|
||||
const addedLen = range.replace([partCreator.pillCandidate(range.text)]);
|
||||
return model.positionForOffset(caret.offset + addedLen, true);
|
||||
});
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
|
||||
// Don't try to do things with the autocomplete if there is none shown
|
||||
if (model.autoComplete) {
|
||||
await model.autoComplete.onTab();
|
||||
if (!model.autoComplete.hasSelection()) {
|
||||
this.setState({showVisualBell: true});
|
||||
model.autoComplete.close();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@ -491,6 +509,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener("selectionchange", this._onSelectionChange);
|
||||
this._editorRef.removeEventListener("input", this._onInput, true);
|
||||
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
|
||||
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
|
||||
@ -545,6 +564,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||
return;
|
||||
}
|
||||
this.historyManager.ensureLastChangesPushed(this.props.model);
|
||||
this._modifiedFlag = true;
|
||||
switch (action) {
|
||||
case "bold":
|
||||
toggleInlineFormat(range, "**");
|
||||
|
@ -175,7 +175,8 @@ const EntityTile = createReactClass({
|
||||
|
||||
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
|
||||
|
||||
const av = this.props.avatarJsx || <BaseAvatar name={this.props.name} width={36} height={36} />;
|
||||
const av = this.props.avatarJsx ||
|
||||
<BaseAvatar name={this.props.name} width={36} height={36} aria-hidden="true" />;
|
||||
|
||||
// The wrapping div is required to make the magic mouse listener work, for some reason.
|
||||
return (
|
||||
|
@ -48,8 +48,6 @@ const eventTileTypes = {
|
||||
|
||||
const stateEventTileTypes = {
|
||||
'm.room.encryption': 'messages.EncryptionEvent',
|
||||
'm.room.aliases': 'messages.TextualEvent',
|
||||
// 'm.room.aliases': 'messages.RoomAliasesEvent', // too complex
|
||||
'm.room.canonical_alias': 'messages.TextualEvent',
|
||||
'm.room.create': 'messages.RoomCreate',
|
||||
'm.room.member': 'messages.TextualEvent',
|
||||
@ -100,6 +98,17 @@ export function getHandlerTile(ev) {
|
||||
}
|
||||
}
|
||||
|
||||
// sometimes MKeyVerificationConclusion declines to render. Jankily decline to render and
|
||||
// fall back to showing hidden events, if we're viewing hidden events
|
||||
// XXX: This is extremely a hack. Possibly these components should have an interface for
|
||||
// declining to render?
|
||||
if (type === "m.key.verification.cancel" && SettingsStore.getValue("showHiddenEventsInTimeline")) {
|
||||
const MKeyVerificationConclusion = sdk.getComponent("messages.MKeyVerificationConclusion");
|
||||
if (!MKeyVerificationConclusion.prototype._shouldRender.call(null, ev, ev.request)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return ev.isState() ? stateEventTileTypes[type] : eventTileTypes[type];
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user