Merge branch 'develop' into feature/padLockInviteOnly
This commit is contained in:
commit
d2e7cc7f19
138 changed files with 4634 additions and 1211 deletions
|
@ -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
|
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/ContentRules-test.js
|
||||||
test/notifications/PushRuleVectorState-test.js
|
test/notifications/PushRuleVectorState-test.js
|
||||||
test/stores/RoomViewStore-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/
|
||||||
|
|
410
CHANGELOG.md
410
CHANGELOG.md
|
@ -1,3 +1,413 @@
|
||||||
|
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)
|
||||||
|
|
||||||
|
* show spinner while loading local aliases
|
||||||
|
[\#4090](https://github.com/matrix-org/matrix-react-sdk/pull/4090)
|
||||||
|
* Don't index key verification events.
|
||||||
|
[\#4083](https://github.com/matrix-org/matrix-react-sdk/pull/4083)
|
||||||
|
* Get rid of dependence on usercontent.riot.im
|
||||||
|
[\#4046](https://github.com/matrix-org/matrix-react-sdk/pull/4046)
|
||||||
|
* also detect aliases using new /aliases endpoint for room access settings
|
||||||
|
[\#4089](https://github.com/matrix-org/matrix-react-sdk/pull/4089)
|
||||||
|
* get local aliases from /aliases in room settings
|
||||||
|
[\#4086](https://github.com/matrix-org/matrix-react-sdk/pull/4086)
|
||||||
|
* Start verification sessions in an E2E DM where possible
|
||||||
|
[\#4080](https://github.com/matrix-org/matrix-react-sdk/pull/4080)
|
||||||
|
* Only show supported verification methods
|
||||||
|
[\#4077](https://github.com/matrix-org/matrix-react-sdk/pull/4077)
|
||||||
|
* Use local echo in VerificationRequest for accepting/declining a verification
|
||||||
|
request
|
||||||
|
[\#4072](https://github.com/matrix-org/matrix-react-sdk/pull/4072)
|
||||||
|
* Report installed PWA, touch input status in rageshakes, analytics
|
||||||
|
[\#4078](https://github.com/matrix-org/matrix-react-sdk/pull/4078)
|
||||||
|
* refactor event grouping into separate helper classes
|
||||||
|
[\#4059](https://github.com/matrix-org/matrix-react-sdk/pull/4059)
|
||||||
|
* Find existing requests when starting a new verification request
|
||||||
|
[\#4070](https://github.com/matrix-org/matrix-react-sdk/pull/4070)
|
||||||
|
* Always speak the full text of the typing indicator when it updates.
|
||||||
|
[\#4074](https://github.com/matrix-org/matrix-react-sdk/pull/4074)
|
||||||
|
* Fix escaped markdown passing backslashes through
|
||||||
|
[\#4008](https://github.com/matrix-org/matrix-react-sdk/pull/4008)
|
||||||
|
* Move the sidebar to below the sidebar tab buttons for screen readers.
|
||||||
|
[\#4071](https://github.com/matrix-org/matrix-react-sdk/pull/4071)
|
||||||
|
|
||||||
|
Changes in [2.1.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.0) (2020-02-17)
|
||||||
|
===================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0-rc.2...v2.1.0)
|
||||||
|
|
||||||
|
* Automate SDK dep upgrades for release
|
||||||
|
[\#4076](https://github.com/matrix-org/matrix-react-sdk/pull/4076)
|
||||||
|
|
||||||
|
Changes in [2.1.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.0-rc.2) (2020-02-13)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0-rc.1...v2.1.0-rc.2)
|
||||||
|
|
||||||
|
* Fix error in previous attempt to upgrade JS SDK
|
||||||
|
|
||||||
|
Changes in [2.1.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.0-rc.1) (2020-02-13)
|
||||||
|
=============================================================================================================
|
||||||
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0...v2.1.0-rc.1)
|
||||||
|
|
||||||
|
* Upgrade JS SDK to 5.0.0-rc.1
|
||||||
|
* don't show tooltips on big icons
|
||||||
|
[\#4067](https://github.com/matrix-org/matrix-react-sdk/pull/4067)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#4069](https://github.com/matrix-org/matrix-react-sdk/pull/4069)
|
||||||
|
* Fix sending of visit variables to Matomo
|
||||||
|
[\#4068](https://github.com/matrix-org/matrix-react-sdk/pull/4068)
|
||||||
|
* Use embedded piwik script rather than piwik.js to respect CSP
|
||||||
|
[\#4066](https://github.com/matrix-org/matrix-react-sdk/pull/4066)
|
||||||
|
* remove methods arg to requestVerification(DM)
|
||||||
|
[\#4058](https://github.com/matrix-org/matrix-react-sdk/pull/4058)
|
||||||
|
* Check for null config settings a bit safer
|
||||||
|
[\#4061](https://github.com/matrix-org/matrix-react-sdk/pull/4061)
|
||||||
|
* Score user ID searches higher when they match nearly exactly
|
||||||
|
[\#4060](https://github.com/matrix-org/matrix-react-sdk/pull/4060)
|
||||||
|
* Fix uncentered letter inside avatar for currently typing users
|
||||||
|
[\#4051](https://github.com/matrix-org/matrix-react-sdk/pull/4051)
|
||||||
|
* Disable 'start' button after clicking in VerificationPanel
|
||||||
|
[\#4065](https://github.com/matrix-org/matrix-react-sdk/pull/4065)
|
||||||
|
* Fixed bug where key reset didn't always return the right key
|
||||||
|
[\#4057](https://github.com/matrix-org/matrix-react-sdk/pull/4057)
|
||||||
|
* Don't render avatars in pills for screen readers.
|
||||||
|
[\#4062](https://github.com/matrix-org/matrix-react-sdk/pull/4062)
|
||||||
|
* Make QR self-verification compatible with RiotX
|
||||||
|
[\#4044](https://github.com/matrix-org/matrix-react-sdk/pull/4044)
|
||||||
|
* Verify single device from other user in right panel & Not Trusted dialog
|
||||||
|
[\#4043](https://github.com/matrix-org/matrix-react-sdk/pull/4043)
|
||||||
|
* Disable verification buttons after clicking to avoid double submission
|
||||||
|
[\#4049](https://github.com/matrix-org/matrix-react-sdk/pull/4049)
|
||||||
|
* Verification toast fixes
|
||||||
|
[\#4048](https://github.com/matrix-org/matrix-react-sdk/pull/4048)
|
||||||
|
* Use EncryptionPanel everywhere, part I
|
||||||
|
[\#4042](https://github.com/matrix-org/matrix-react-sdk/pull/4042)
|
||||||
|
* quick fix for cross-signing reset bug
|
||||||
|
[\#4056](https://github.com/matrix-org/matrix-react-sdk/pull/4056)
|
||||||
|
* Fix error message rendering for key entry
|
||||||
|
[\#4055](https://github.com/matrix-org/matrix-react-sdk/pull/4055)
|
||||||
|
* Fix recaptcha blocked by CSP for non-SSL origins
|
||||||
|
[\#4052](https://github.com/matrix-org/matrix-react-sdk/pull/4052)
|
||||||
|
* Fix watcher for showTypingNotifications setting
|
||||||
|
[\#4054](https://github.com/matrix-org/matrix-react-sdk/pull/4054)
|
||||||
|
* Allow custom hs url submission on enter
|
||||||
|
[\#4053](https://github.com/matrix-org/matrix-react-sdk/pull/4053)
|
||||||
|
* Support keepSecretStoragePassphraseForSession at the config level too
|
||||||
|
[\#4045](https://github.com/matrix-org/matrix-react-sdk/pull/4045)
|
||||||
|
* Add setting to allow hiding of typing indicator
|
||||||
|
[\#4047](https://github.com/matrix-org/matrix-react-sdk/pull/4047)
|
||||||
|
* Button to reset cross-signing and SSSS keys
|
||||||
|
[\#4041](https://github.com/matrix-org/matrix-react-sdk/pull/4041)
|
||||||
|
* Use forms to wrap password fields so Chrome doesn't go wild
|
||||||
|
[\#3974](https://github.com/matrix-org/matrix-react-sdk/pull/3974)
|
||||||
|
* Update QR code rendering to support VerificationRequests
|
||||||
|
[\#4001](https://github.com/matrix-org/matrix-react-sdk/pull/4001)
|
||||||
|
* Differentiate AccessSecretStorageDialog dismiss dialog based on which key we
|
||||||
|
want to read
|
||||||
|
[\#4038](https://github.com/matrix-org/matrix-react-sdk/pull/4038)
|
||||||
|
* Only emit in RoomViewStore when state actually changes
|
||||||
|
[\#4039](https://github.com/matrix-org/matrix-react-sdk/pull/4039)
|
||||||
|
* Mark AccessSecretStorageDialog to not be closed by clicking background
|
||||||
|
[\#4029](https://github.com/matrix-org/matrix-react-sdk/pull/4029)
|
||||||
|
* Let pointer events fall through to scroll button
|
||||||
|
[\#4037](https://github.com/matrix-org/matrix-react-sdk/pull/4037)
|
||||||
|
* Improve event indexing status strings for translation
|
||||||
|
[\#4035](https://github.com/matrix-org/matrix-react-sdk/pull/4035)
|
||||||
|
* Button size reviewed for word consuming languages & Settings showing devices
|
||||||
|
are a bit too tight
|
||||||
|
[\#4024](https://github.com/matrix-org/matrix-react-sdk/pull/4024)
|
||||||
|
* Only enumerate settings handlers which are supported
|
||||||
|
[\#4034](https://github.com/matrix-org/matrix-react-sdk/pull/4034)
|
||||||
|
* Fix listener removal in verification tile
|
||||||
|
[\#4036](https://github.com/matrix-org/matrix-react-sdk/pull/4036)
|
||||||
|
* Do not show alarming red shields on large encrypted rooms for your own
|
||||||
|
device
|
||||||
|
[\#4028](https://github.com/matrix-org/matrix-react-sdk/pull/4028)
|
||||||
|
* Add a class for styling room directory permissions
|
||||||
|
[\#4007](https://github.com/matrix-org/matrix-react-sdk/pull/4007)
|
||||||
|
* double-check user verification
|
||||||
|
[\#4010](https://github.com/matrix-org/matrix-react-sdk/pull/4010)
|
||||||
|
* Use minimist instead of optimist as it is deprecated
|
||||||
|
[\#4031](https://github.com/matrix-org/matrix-react-sdk/pull/4031)
|
||||||
|
* SettingsStore, use a counter instead of wall clock for watcher ids
|
||||||
|
[\#4032](https://github.com/matrix-org/matrix-react-sdk/pull/4032)
|
||||||
|
* Don't crash immediately if the room directory chunk is null/empty
|
||||||
|
[\#4027](https://github.com/matrix-org/matrix-react-sdk/pull/4027)
|
||||||
|
* Fix verification toast to close at 0s
|
||||||
|
[\#3998](https://github.com/matrix-org/matrix-react-sdk/pull/3998)
|
||||||
|
* Fix listener leak in TagPanel
|
||||||
|
[\#4026](https://github.com/matrix-org/matrix-react-sdk/pull/4026)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#4025](https://github.com/matrix-org/matrix-react-sdk/pull/4025)
|
||||||
|
* Honour the isLogin flag in theme.js
|
||||||
|
[\#4023](https://github.com/matrix-org/matrix-react-sdk/pull/4023)
|
||||||
|
* ManageEventIndexDialog: Show how many rooms are being currently crawled.
|
||||||
|
[\#4022](https://github.com/matrix-org/matrix-react-sdk/pull/4022)
|
||||||
|
* Advertise that we can scan QR codes even though we can't
|
||||||
|
[\#4021](https://github.com/matrix-org/matrix-react-sdk/pull/4021)
|
||||||
|
* Checkpoint addition fixes and return of the crawler sleep time setting.
|
||||||
|
[\#4020](https://github.com/matrix-org/matrix-react-sdk/pull/4020)
|
||||||
|
* Truncate SAS emoji labels to fit
|
||||||
|
[\#4018](https://github.com/matrix-org/matrix-react-sdk/pull/4018)
|
||||||
|
* Apply copy edits to security setup flow
|
||||||
|
[\#4017](https://github.com/matrix-org/matrix-react-sdk/pull/4017)
|
||||||
|
* Fix user trust text to match what was checked
|
||||||
|
[\#4016](https://github.com/matrix-org/matrix-react-sdk/pull/4016)
|
||||||
|
* Fix size of invite only icon
|
||||||
|
[\#4015](https://github.com/matrix-org/matrix-react-sdk/pull/4015)
|
||||||
|
* Add temporary feature flag to control padlocks
|
||||||
|
[\#4013](https://github.com/matrix-org/matrix-react-sdk/pull/4013)
|
||||||
|
* Add an override for the theme
|
||||||
|
[\#4014](https://github.com/matrix-org/matrix-react-sdk/pull/4014)
|
||||||
|
* Add title to complete security loading
|
||||||
|
[\#4011](https://github.com/matrix-org/matrix-react-sdk/pull/4011)
|
||||||
|
* Only display the first zxcvbn warning/suggestion
|
||||||
|
[\#4012](https://github.com/matrix-org/matrix-react-sdk/pull/4012)
|
||||||
|
* Log exceptions from accessSecretStorage
|
||||||
|
[\#4009](https://github.com/matrix-org/matrix-react-sdk/pull/4009)
|
||||||
|
* Add advanced option to keep secret storage in memory for session
|
||||||
|
[\#3995](https://github.com/matrix-org/matrix-react-sdk/pull/3995)
|
||||||
|
* Add shields to member list, move power label to text
|
||||||
|
[\#4006](https://github.com/matrix-org/matrix-react-sdk/pull/4006)
|
||||||
|
* Make encryption events into bubble-style tiles
|
||||||
|
[\#4005](https://github.com/matrix-org/matrix-react-sdk/pull/4005)
|
||||||
|
* Update copy when the user verifies their own devices
|
||||||
|
[\#4000](https://github.com/matrix-org/matrix-react-sdk/pull/4000)
|
||||||
|
* Use Sets instead of array scans and simplify hiding of invalid users when
|
||||||
|
inviting
|
||||||
|
[\#4004](https://github.com/matrix-org/matrix-react-sdk/pull/4004)
|
||||||
|
* Fix room completion for invited rooms and upgraded rooms
|
||||||
|
[\#4003](https://github.com/matrix-org/matrix-react-sdk/pull/4003)
|
||||||
|
* Make shields in UserInfo black if user isn't verified
|
||||||
|
[\#3999](https://github.com/matrix-org/matrix-react-sdk/pull/3999)
|
||||||
|
* Change verify user text
|
||||||
|
[\#3994](https://github.com/matrix-org/matrix-react-sdk/pull/3994)
|
||||||
|
* Disable all inputs in login form while busy, not just the submit button
|
||||||
|
[\#3996](https://github.com/matrix-org/matrix-react-sdk/pull/3996)
|
||||||
|
* fix SAS dialog width
|
||||||
|
[\#3993](https://github.com/matrix-org/matrix-react-sdk/pull/3993)
|
||||||
|
* Update placeholder in the composer when it gets changed
|
||||||
|
[\#3990](https://github.com/matrix-org/matrix-react-sdk/pull/3990)
|
||||||
|
* Send initial device display name on register
|
||||||
|
[\#3992](https://github.com/matrix-org/matrix-react-sdk/pull/3992)
|
||||||
|
* Update QR code handling for new spec
|
||||||
|
[\#3959](https://github.com/matrix-org/matrix-react-sdk/pull/3959)
|
||||||
|
* Apply the Olympic effect to SAS Emoji Verification
|
||||||
|
[\#3989](https://github.com/matrix-org/matrix-react-sdk/pull/3989)
|
||||||
|
* Pass an ID to the <Field/> as needed and fix div inside p nesting
|
||||||
|
[\#3988](https://github.com/matrix-org/matrix-react-sdk/pull/3988)
|
||||||
|
* Update user info for device and trust changes
|
||||||
|
[\#3987](https://github.com/matrix-org/matrix-react-sdk/pull/3987)
|
||||||
|
* Relax secret storage account data check
|
||||||
|
[\#3985](https://github.com/matrix-org/matrix-react-sdk/pull/3985)
|
||||||
|
* Fix various races that prevented the right panel being in the right state
|
||||||
|
for verifications
|
||||||
|
[\#3984](https://github.com/matrix-org/matrix-react-sdk/pull/3984)
|
||||||
|
* Fix verifying individual devices
|
||||||
|
[\#3986](https://github.com/matrix-org/matrix-react-sdk/pull/3986)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#3982](https://github.com/matrix-org/matrix-react-sdk/pull/3982)
|
||||||
|
* Replace device with session in UI text
|
||||||
|
[\#3980](https://github.com/matrix-org/matrix-react-sdk/pull/3980)
|
||||||
|
* Add missing await causing promises to be leaked as room IDs
|
||||||
|
[\#3981](https://github.com/matrix-org/matrix-react-sdk/pull/3981)
|
||||||
|
* Change new session toast to unverified
|
||||||
|
[\#3978](https://github.com/matrix-org/matrix-react-sdk/pull/3978)
|
||||||
|
* Replace Verify button in UserInfo verification with "Learn more"
|
||||||
|
[\#3975](https://github.com/matrix-org/matrix-react-sdk/pull/3975)
|
||||||
|
* Don't peek until the matrix client is ready
|
||||||
|
[\#3979](https://github.com/matrix-org/matrix-react-sdk/pull/3979)
|
||||||
|
* Verification: don't block UI update on verification finishing
|
||||||
|
[\#3976](https://github.com/matrix-org/matrix-react-sdk/pull/3976)
|
||||||
|
* Adjust icons with in person with design
|
||||||
|
[\#3977](https://github.com/matrix-org/matrix-react-sdk/pull/3977)
|
||||||
|
* Update copy for right panel verification
|
||||||
|
[\#3973](https://github.com/matrix-org/matrix-react-sdk/pull/3973)
|
||||||
|
* Check for timeline in pre-join UISI path
|
||||||
|
[\#3972](https://github.com/matrix-org/matrix-react-sdk/pull/3972)
|
||||||
|
* Let users paste text if they've already started filtering invite targets
|
||||||
|
[\#3970](https://github.com/matrix-org/matrix-react-sdk/pull/3970)
|
||||||
|
* Filter event types when deciding on activity metrics for DM suggestions
|
||||||
|
[\#3969](https://github.com/matrix-org/matrix-react-sdk/pull/3969)
|
||||||
|
* Revert a change causing a login loop
|
||||||
|
[\#3971](https://github.com/matrix-org/matrix-react-sdk/pull/3971)
|
||||||
|
* Improve the docs for the event index and fix some type hints.
|
||||||
|
[\#3960](https://github.com/matrix-org/matrix-react-sdk/pull/3960)
|
||||||
|
* Automatically focus on the invite dialog input
|
||||||
|
[\#3968](https://github.com/matrix-org/matrix-react-sdk/pull/3968)
|
||||||
|
* Restore key backup in Complete Security dialog
|
||||||
|
[\#3966](https://github.com/matrix-org/matrix-react-sdk/pull/3966)
|
||||||
|
* Right Panel Verification improvements
|
||||||
|
[\#3967](https://github.com/matrix-org/matrix-react-sdk/pull/3967)
|
||||||
|
* Cross Signing Right Panel Verification Decoration
|
||||||
|
[\#3950](https://github.com/matrix-org/matrix-react-sdk/pull/3950)
|
||||||
|
* Passing refireParams actually prevented this from working
|
||||||
|
[\#3965](https://github.com/matrix-org/matrix-react-sdk/pull/3965)
|
||||||
|
* Start new key backup in security setup flow
|
||||||
|
[\#3964](https://github.com/matrix-org/matrix-react-sdk/pull/3964)
|
||||||
|
* Tweak styling of the unread indicator circle.
|
||||||
|
[\#3958](https://github.com/matrix-org/matrix-react-sdk/pull/3958)
|
||||||
|
* Add device IDs in user info tooltips
|
||||||
|
[\#3963](https://github.com/matrix-org/matrix-react-sdk/pull/3963)
|
||||||
|
* Improve encryption upgrade on login flow
|
||||||
|
[\#3962](https://github.com/matrix-org/matrix-react-sdk/pull/3962)
|
||||||
|
* Switch back to legacy decorators
|
||||||
|
[\#3961](https://github.com/matrix-org/matrix-react-sdk/pull/3961)
|
||||||
|
* Style bridge settings tab according to design
|
||||||
|
[\#3894](https://github.com/matrix-org/matrix-react-sdk/pull/3894)
|
||||||
|
* Fix skinning and babel targets
|
||||||
|
[\#3957](https://github.com/matrix-org/matrix-react-sdk/pull/3957)
|
||||||
|
* Enable cross-signing lab when key in storage
|
||||||
|
[\#3956](https://github.com/matrix-org/matrix-react-sdk/pull/3956)
|
||||||
|
* Add new session verification details dialog
|
||||||
|
[\#3953](https://github.com/matrix-org/matrix-react-sdk/pull/3953)
|
||||||
|
* Fix issue where we don't notice if our own devices shouldn't be trusted
|
||||||
|
[\#3949](https://github.com/matrix-org/matrix-react-sdk/pull/3949)
|
||||||
|
* Add separate component for post-auth security flows
|
||||||
|
[\#3951](https://github.com/matrix-org/matrix-react-sdk/pull/3951)
|
||||||
|
* Add more logging to settings watchers
|
||||||
|
[\#3952](https://github.com/matrix-org/matrix-react-sdk/pull/3952)
|
||||||
|
* Use https for recaptcha for all non-http protocols
|
||||||
|
[\#3944](https://github.com/matrix-org/matrix-react-sdk/pull/3944)
|
||||||
|
* Add status and management UI for the event indexer
|
||||||
|
[\#3672](https://github.com/matrix-org/matrix-react-sdk/pull/3672)
|
||||||
|
* Remove DM icons if `feature_cross_signing` is enabled; hide padlocks in DM
|
||||||
|
room headers
|
||||||
|
[\#3948](https://github.com/matrix-org/matrix-react-sdk/pull/3948)
|
||||||
|
* Stop rogue verification toast if you verify during login
|
||||||
|
[\#3943](https://github.com/matrix-org/matrix-react-sdk/pull/3943)
|
||||||
|
* Show incoming verification requests in the 'complete security' phase
|
||||||
|
[\#3942](https://github.com/matrix-org/matrix-react-sdk/pull/3942)
|
||||||
|
* Dismiss logged out device toasts
|
||||||
|
[\#3941](https://github.com/matrix-org/matrix-react-sdk/pull/3941)
|
||||||
|
* Verification nag toasts
|
||||||
|
[\#3940](https://github.com/matrix-org/matrix-react-sdk/pull/3940)
|
||||||
|
* Update from Weblate
|
||||||
|
[\#3947](https://github.com/matrix-org/matrix-react-sdk/pull/3947)
|
||||||
|
* Remember password for e2e bootstrapping
|
||||||
|
[\#3939](https://github.com/matrix-org/matrix-react-sdk/pull/3939)
|
||||||
|
* fix compound emoji
|
||||||
|
[\#3946](https://github.com/matrix-org/matrix-react-sdk/pull/3946)
|
||||||
|
* Setup flow for cross-signing on login / registration
|
||||||
|
[\#3937](https://github.com/matrix-org/matrix-react-sdk/pull/3937)
|
||||||
|
* Update profile avatar letter size
|
||||||
|
[\#3935](https://github.com/matrix-org/matrix-react-sdk/pull/3935)
|
||||||
|
* Hide default encryption algorithm
|
||||||
|
[\#3936](https://github.com/matrix-org/matrix-react-sdk/pull/3936)
|
||||||
|
* Resolve default export warnings from Webpack
|
||||||
|
[\#3938](https://github.com/matrix-org/matrix-react-sdk/pull/3938)
|
||||||
|
* Add null check for cross-signing info in verification panel
|
||||||
|
[\#3934](https://github.com/matrix-org/matrix-react-sdk/pull/3934)
|
||||||
|
* Add trace logging to figure out which component is causing weird events
|
||||||
|
[\#3926](https://github.com/matrix-org/matrix-react-sdk/pull/3926)
|
||||||
|
* Remove user lists feature flag, making it the default
|
||||||
|
[\#3906](https://github.com/matrix-org/matrix-react-sdk/pull/3906)
|
||||||
|
* Last bit of polish for user lists
|
||||||
|
[\#3925](https://github.com/matrix-org/matrix-react-sdk/pull/3925)
|
||||||
|
* QR code verification
|
||||||
|
[\#3871](https://github.com/matrix-org/matrix-react-sdk/pull/3871)
|
||||||
|
* Do less unnecessary work on CI
|
||||||
|
[\#3933](https://github.com/matrix-org/matrix-react-sdk/pull/3933)
|
||||||
|
* Re-enable stylelint on CI
|
||||||
|
[\#3932](https://github.com/matrix-org/matrix-react-sdk/pull/3932)
|
||||||
|
* Design pass for room icons
|
||||||
|
[\#3931](https://github.com/matrix-org/matrix-react-sdk/pull/3931)
|
||||||
|
* Populate the file panel using the event index if available.
|
||||||
|
[\#3858](https://github.com/matrix-org/matrix-react-sdk/pull/3858)
|
||||||
|
* Split AsyncWrapper out from Modal
|
||||||
|
[\#3928](https://github.com/matrix-org/matrix-react-sdk/pull/3928)
|
||||||
|
* Fix error in verification code on develop
|
||||||
|
[\#3930](https://github.com/matrix-org/matrix-react-sdk/pull/3930)
|
||||||
|
* Seperates out the padlock icon, and adds a tooltip
|
||||||
|
[\#3929](https://github.com/matrix-org/matrix-react-sdk/pull/3929)
|
||||||
|
* Cross Signing redesign for composer
|
||||||
|
[\#3910](https://github.com/matrix-org/matrix-react-sdk/pull/3910)
|
||||||
|
* Fix verifying your own devices with to_device messages
|
||||||
|
[\#3927](https://github.com/matrix-org/matrix-react-sdk/pull/3927)
|
||||||
|
* Room list reflects encryption state
|
||||||
|
[\#3908](https://github.com/matrix-org/matrix-react-sdk/pull/3908)
|
||||||
|
* Make the entire User Info scrollable, sticky close button
|
||||||
|
[\#3914](https://github.com/matrix-org/matrix-react-sdk/pull/3914)
|
||||||
|
* Remove riot logo from the security setup screens
|
||||||
|
[\#3916](https://github.com/matrix-org/matrix-react-sdk/pull/3916)
|
||||||
|
* Only say the session is verified if it is now verified
|
||||||
|
[\#3917](https://github.com/matrix-org/matrix-react-sdk/pull/3917)
|
||||||
|
* Hide password section if you can't change your password
|
||||||
|
[\#3924](https://github.com/matrix-org/matrix-react-sdk/pull/3924)
|
||||||
|
* Ensure a plaintext version of the composer ends up on the clipboard
|
||||||
|
[\#3922](https://github.com/matrix-org/matrix-react-sdk/pull/3922)
|
||||||
|
* Move & upgrade babel runtime into dependencies (like it wants)
|
||||||
|
[\#3920](https://github.com/matrix-org/matrix-react-sdk/pull/3920)
|
||||||
|
* Don't list every single alias when there's many
|
||||||
|
[\#3918](https://github.com/matrix-org/matrix-react-sdk/pull/3918)
|
||||||
|
* Try to populate user IDs even when the server's directory fails us
|
||||||
|
[\#3907](https://github.com/matrix-org/matrix-react-sdk/pull/3907)
|
||||||
|
* Remove .event property on verification request
|
||||||
|
[\#3912](https://github.com/matrix-org/matrix-react-sdk/pull/3912)
|
||||||
|
* Attempt to fix Safari + VoiceOver misunderstanding the timeline list
|
||||||
|
[\#3911](https://github.com/matrix-org/matrix-react-sdk/pull/3911)
|
||||||
|
* Enable encryption in DMs with device keys
|
||||||
|
[\#3913](https://github.com/matrix-org/matrix-react-sdk/pull/3913)
|
||||||
|
* Fix scrollable area and padding in user lists dialog
|
||||||
|
[\#3905](https://github.com/matrix-org/matrix-react-sdk/pull/3905)
|
||||||
|
* Add Reject & Ignore user button to invites view
|
||||||
|
[\#3909](https://github.com/matrix-org/matrix-react-sdk/pull/3909)
|
||||||
|
* Fix paragraph-awareness of the composer formatting features
|
||||||
|
[\#3891](https://github.com/matrix-org/matrix-react-sdk/pull/3891)
|
||||||
|
* Updated visuals for cross-signing bootstrap
|
||||||
|
[\#3903](https://github.com/matrix-org/matrix-react-sdk/pull/3903)
|
||||||
|
* Implement some parts of new cross signing bootstrap UI
|
||||||
|
[\#3897](https://github.com/matrix-org/matrix-react-sdk/pull/3897)
|
||||||
|
* Treat links as external in report content admin message
|
||||||
|
[\#3904](https://github.com/matrix-org/matrix-react-sdk/pull/3904)
|
||||||
|
* Be consistent about our settings svg, free the other one
|
||||||
|
[\#3902](https://github.com/matrix-org/matrix-react-sdk/pull/3902)
|
||||||
|
* Change prepublish script to prepare
|
||||||
|
[\#3899](https://github.com/matrix-org/matrix-react-sdk/pull/3899)
|
||||||
|
* Remove the react-sdk version
|
||||||
|
[\#3901](https://github.com/matrix-org/matrix-react-sdk/pull/3901)
|
||||||
|
* BuildKite: Retry end-to-end tests automatically once if they fail
|
||||||
|
[\#3900](https://github.com/matrix-org/matrix-react-sdk/pull/3900)
|
||||||
|
* Slash Command improvements around sending messages with leading slash
|
||||||
|
[\#3893](https://github.com/matrix-org/matrix-react-sdk/pull/3893)
|
||||||
|
* Support admin configurable message when reporting content
|
||||||
|
[\#3898](https://github.com/matrix-org/matrix-react-sdk/pull/3898)
|
||||||
|
* Don't warn on unverified users; ensured behavior stays the same with flags
|
||||||
|
off
|
||||||
|
[\#3896](https://github.com/matrix-org/matrix-react-sdk/pull/3896)
|
||||||
|
* Fix roving room list for resizer and ff tabstop a11y
|
||||||
|
[\#3895](https://github.com/matrix-org/matrix-react-sdk/pull/3895)
|
||||||
|
* Verify individual messages via cross-signing
|
||||||
|
[\#3875](https://github.com/matrix-org/matrix-react-sdk/pull/3875)
|
||||||
|
* Fix layering of dependencies in riot-web and e2e tests
|
||||||
|
[\#3882](https://github.com/matrix-org/matrix-react-sdk/pull/3882)
|
||||||
|
* Implement Roving Tab Index and Room List as TreeView
|
||||||
|
[\#3844](https://github.com/matrix-org/matrix-react-sdk/pull/3844)
|
||||||
|
* Move room header shields over the avatar for the room
|
||||||
|
[\#3888](https://github.com/matrix-org/matrix-react-sdk/pull/3888)
|
||||||
|
* Fix toast icon to prevent clipping
|
||||||
|
[\#3890](https://github.com/matrix-org/matrix-react-sdk/pull/3890)
|
||||||
|
* Only show devices and verify actions in E2EE rooms
|
||||||
|
[\#3889](https://github.com/matrix-org/matrix-react-sdk/pull/3889)
|
||||||
|
* Change user info verification checks to use cross-signing
|
||||||
|
[\#3887](https://github.com/matrix-org/matrix-react-sdk/pull/3887)
|
||||||
|
* Fix click-to-ping not inserting colon if composer non-empty
|
||||||
|
[\#3886](https://github.com/matrix-org/matrix-react-sdk/pull/3886)
|
||||||
|
* Fix emoticon space completion for upper case emoticons like :D xD
|
||||||
|
[\#3884](https://github.com/matrix-org/matrix-react-sdk/pull/3884)
|
||||||
|
* Repair cross-signing panel with async status
|
||||||
|
[\#3880](https://github.com/matrix-org/matrix-react-sdk/pull/3880)
|
||||||
|
* Remove temporary key backup button
|
||||||
|
[\#3878](https://github.com/matrix-org/matrix-react-sdk/pull/3878)
|
||||||
|
* Score users who have recently spoken higher in invite suggestions
|
||||||
|
[\#3866](https://github.com/matrix-org/matrix-react-sdk/pull/3866)
|
||||||
|
* Initial support for verification in right panel
|
||||||
|
[\#3796](https://github.com/matrix-org/matrix-react-sdk/pull/3796)
|
||||||
|
* Prevent the invite dialog from jumping around when elements change
|
||||||
|
[\#3868](https://github.com/matrix-org/matrix-react-sdk/pull/3868)
|
||||||
|
* Add prepublish script
|
||||||
|
[\#3876](https://github.com/matrix-org/matrix-react-sdk/pull/3876)
|
||||||
|
|
||||||
Changes in [2.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0) (2020-01-27)
|
Changes in [2.0.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.0.0) (2020-01-27)
|
||||||
===================================================================================================
|
===================================================================================================
|
||||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.2...v2.0.0)
|
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.2...v2.0.0)
|
||||||
|
|
27
docs/usercontent.md
Normal file
27
docs/usercontent.md
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
# Usercontent
|
||||||
|
|
||||||
|
While decryption itself is safe to be done without a sandbox,
|
||||||
|
letting the browser and user interact with the resulting data may be dangerous,
|
||||||
|
previously `usercontent.riot.im` was used to act as a sandbox on a different origin to close the attack surface,
|
||||||
|
it is now possible to do by using a combination of a sandboxed iframe and some code written into the app which consumes this SDK.
|
||||||
|
|
||||||
|
Usercontent is an iframe sandbox target for allowing a user to safely download a decrypted attachment from a sandboxed origin where it cannot be used to XSS your riot session out from under you.
|
||||||
|
|
||||||
|
Its function is to create an Object URL for the user/browser to use but bound to an origin different to that of the riot instance to protect against XSS.
|
||||||
|
|
||||||
|
It exposes a function over a postMessage API, when sent an object with the matching fields to render a download link with the Object URL:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
"imgSrc": "", // the src of the image to display in the download link
|
||||||
|
"imgStyle": "", // the style to apply to the image
|
||||||
|
"style": "", // the style to apply to the download link
|
||||||
|
"download": "", // download attribute to pass to the <a/> tag
|
||||||
|
"textContent": "", // the text to put inside the download link
|
||||||
|
"blob": "", // the data blob to wrap in an object url and allow the user to download
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If only imgSrc, imgStyle and style are passed then just update the existing link without overwriting other things about it.
|
||||||
|
|
||||||
|
It is expected that this target be available at `usercontent/` relative to the root of the app, this can be seen in riot-web's webpack config.
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "2.0.0",
|
"version": "2.1.1",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -85,6 +85,7 @@
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.5",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.5.8",
|
||||||
|
"qrcode": "^1.4.4",
|
||||||
"qrcode-react": "^0.1.16",
|
"qrcode-react": "^0.1.16",
|
||||||
"qs": "^6.6.0",
|
"qs": "^6.6.0",
|
||||||
"react": "^16.9.0",
|
"react": "^16.9.0",
|
||||||
|
|
49
release.sh
49
release.sh
|
@ -9,4 +9,51 @@ set -e
|
||||||
|
|
||||||
cd `dirname $0`
|
cd `dirname $0`
|
||||||
|
|
||||||
exec ./node_modules/matrix-js-sdk/release.sh -z "$@"
|
for i in matrix-js-sdk
|
||||||
|
do
|
||||||
|
depver=`cat package.json | jq -r .dependencies[\"$i\"]`
|
||||||
|
latestver=`yarn info -s $i dist-tags.next`
|
||||||
|
if [ "$depver" != "$latestver" ]
|
||||||
|
then
|
||||||
|
echo "The latest version of $i is $latestver but package.json depends on $depver."
|
||||||
|
echo -n "Type 'u' to auto-upgrade, 'c' to continue anyway, or 'a' to abort:"
|
||||||
|
read resp
|
||||||
|
if [ "$resp" != "u" ] && [ "$resp" != "c" ]
|
||||||
|
then
|
||||||
|
echo "Aborting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$resp" == "u" ]
|
||||||
|
then
|
||||||
|
echo "Upgrading $i to $latestver..."
|
||||||
|
yarn add -E $i@$latestver
|
||||||
|
git add -u
|
||||||
|
# The `-e` flag opens the editor and gives you a chance to check
|
||||||
|
# the upgrade for correctness.
|
||||||
|
git commit -m "Upgrade $i to $latestver" -e
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
./node_modules/matrix-js-sdk/release.sh -z "$@"
|
||||||
|
|
||||||
|
release="${1#v}"
|
||||||
|
prerelease=0
|
||||||
|
# We check if this build is a prerelease by looking to
|
||||||
|
# see if the version has a hyphen in it. Crude,
|
||||||
|
# but semver doesn't support postreleases so anything
|
||||||
|
# with a hyphen is a prerelease.
|
||||||
|
echo $release | grep -q '-' && prerelease=1
|
||||||
|
|
||||||
|
if [ $prerelease -eq 0 ]
|
||||||
|
then
|
||||||
|
# For a release, reset SDK deps back to the `develop` branch.
|
||||||
|
for i in matrix-js-sdk
|
||||||
|
do
|
||||||
|
echo "Resetting $i to develop branch..."
|
||||||
|
yarn add github:matrix-org/$i#develop
|
||||||
|
git add -u
|
||||||
|
git commit -m "Reset $i back to develop branch"
|
||||||
|
done
|
||||||
|
git push origin develop
|
||||||
|
fi
|
||||||
|
|
|
@ -119,6 +119,16 @@ limitations under the License.
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomDirectory_perm {
|
||||||
|
border-radius: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
height: 20px;
|
||||||
|
line-height: 20px;
|
||||||
|
padding: 0 5px;
|
||||||
|
color: $accent-fg-color;
|
||||||
|
background-color: $rte-room-pill-color;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_RoomDirectory_topic {
|
.mx_RoomDirectory_topic {
|
||||||
cursor: initial;
|
cursor: initial;
|
||||||
color: $light-fg-color;
|
color: $light-fg-color;
|
||||||
|
|
|
@ -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: ":";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_goButton {
|
.mx_InviteDialog_goButton {
|
||||||
width: 48px;
|
min-width: 48px;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
|
@ -131,7 +131,7 @@ limitations under the License.
|
||||||
height: 24px;
|
height: 24px;
|
||||||
grid-column: 1;
|
grid-column: 1;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
mask-image: url('$(res)/img/feather-customised/check.svg');
|
mask-image: url("$(res)/img/feather-customised/check.svg");
|
||||||
mask-size: 100%;
|
mask-size: 100%;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
|
@ -85,3 +85,9 @@ limitations under the License.
|
||||||
flex: 1;
|
flex: 1;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_CreateKeyBackupDialog {
|
||||||
|
details .mx_AccessibleButton {
|
||||||
|
margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,3 +37,70 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Special case styling for EncryptionPanel in a Modal dialog
|
||||||
|
.mx_Dialog, .mx_CompleteSecurity_body {
|
||||||
|
.mx_VerificationPanel_QRPhase_startOptions {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
> .mx_VerificationPanel_QRPhase_betweenText {
|
||||||
|
width: 50px;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VerificationPanel_QRPhase_startOption {
|
||||||
|
background-color: $user-tile-hover-bg-color;
|
||||||
|
border-radius: 10px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
padding: 10px;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
canvas, .mx_VerificationPanel_QRPhase_noQR {
|
||||||
|
width: 220px !important;
|
||||||
|
height: 220px !important;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_VerificationPanel_QRPhase_helpText {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 71px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 30px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncryptionPanel when verification is done
|
||||||
|
.mx_VerificationPanel_verified_section {
|
||||||
|
// center the big shield icon
|
||||||
|
.mx_E2EIcon {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
// right align the "Got it" button
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -32,9 +32,9 @@ limitations under the License.
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 4px;
|
height: 4px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
overflow: hidden;
|
|
||||||
background-color: $secondary-accent-color;
|
background-color: $secondary-accent-color;
|
||||||
border: 6px solid $accent-color;
|
border: 6px solid $accent-color;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TopUnreadMessagesBar_scrollUp {
|
.mx_TopUnreadMessagesBar_scrollUp {
|
||||||
|
|
|
@ -31,14 +31,15 @@ limitations under the License.
|
||||||
margin-left: -12px;
|
margin-left: -12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_image {
|
|
||||||
border: 1px solid $primary-bg-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_initial {
|
.mx_WhoIsTypingTile_avatars .mx_BaseAvatar_initial {
|
||||||
padding-top: 1px;
|
padding-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_WhoIsTypingTile_avatars .mx_BaseAvatar {
|
||||||
|
border: 1px solid $primary-bg-color;
|
||||||
|
border-radius: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_WhoIsTypingTile_remainingAvatarPlaceholder {
|
.mx_WhoIsTypingTile_remainingAvatarPlaceholder {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
display: table;
|
display: table;
|
||||||
table-layout: fixed;
|
table-layout: fixed;
|
||||||
width: 880px;
|
width: 880px;
|
||||||
border-spacing: 2px;
|
border-spacing: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevicesPanel_header {
|
.mx_DevicesPanel_header {
|
||||||
|
@ -32,7 +32,11 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_DevicesPanel_header > div {
|
.mx_DevicesPanel_header > div {
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
vertical-align: bottom;
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_DevicesPanel_header .mx_DevicesPanel_deviceName {
|
||||||
|
width: 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen {
|
.mx_DevicesPanel_header .mx_DevicesPanel_deviceLastSeen {
|
||||||
|
|
|
@ -14,8 +14,10 @@ echo "generating $out"
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||||
|
|
||||||
EOF
|
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' |
|
jq -r '.[] | select((.errorCount + .warningCount) > 0) | .filePath' |
|
||||||
sed -e 's/.*matrix-react-sdk\///';
|
sed -e 's/.*matrix-react-sdk\///';
|
||||||
} > "$out"
|
} > "$out"
|
||||||
|
# also append rules from eslintignore file
|
||||||
|
cat .eslintignore >> $out
|
||||||
|
|
270
src/Analytics.js
270
src/Analytics.js
|
@ -1,18 +1,21 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
|
@ -54,6 +57,8 @@ function getRedactedUrl() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const customVariables = {
|
const customVariables = {
|
||||||
|
// The Matomo installation at https://matomo.riot.im is currently configured
|
||||||
|
// with a limit of 10 custom variables.
|
||||||
'App Platform': {
|
'App Platform': {
|
||||||
id: 1,
|
id: 1,
|
||||||
expl: _td('The platform you\'re on'),
|
expl: _td('The platform you\'re on'),
|
||||||
|
@ -61,7 +66,7 @@ const customVariables = {
|
||||||
},
|
},
|
||||||
'App Version': {
|
'App Version': {
|
||||||
id: 2,
|
id: 2,
|
||||||
expl: _td('The version of Riot.im'),
|
expl: _td('The version of Riot'),
|
||||||
example: '15.0.0',
|
example: '15.0.0',
|
||||||
},
|
},
|
||||||
'User Type': {
|
'User Type': {
|
||||||
|
@ -84,20 +89,25 @@ const customVariables = {
|
||||||
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
|
||||||
example: 'off',
|
example: 'off',
|
||||||
},
|
},
|
||||||
'Breadcrumbs': {
|
|
||||||
id: 9,
|
|
||||||
expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"),
|
|
||||||
example: 'disabled',
|
|
||||||
},
|
|
||||||
'Homeserver URL': {
|
'Homeserver URL': {
|
||||||
id: 7,
|
id: 7,
|
||||||
expl: _td('Your homeserver\'s URL'),
|
expl: _td('Your homeserver\'s URL'),
|
||||||
example: 'https://matrix.org',
|
example: 'https://matrix.org',
|
||||||
},
|
},
|
||||||
'Identity Server URL': {
|
'Touch Input': {
|
||||||
id: 8,
|
id: 8,
|
||||||
expl: _td('Your identity server\'s URL'),
|
expl: _td("Whether you're using Riot on a device where touch is the primary input mechanism"),
|
||||||
example: 'https://vector.im',
|
example: 'false',
|
||||||
|
},
|
||||||
|
'Breadcrumbs': {
|
||||||
|
id: 9,
|
||||||
|
expl: _td("Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)"),
|
||||||
|
example: 'disabled',
|
||||||
|
},
|
||||||
|
'Installed PWA': {
|
||||||
|
id: 10,
|
||||||
|
expl: _td("Whether you're using Riot as an installed Progressive Web App"),
|
||||||
|
example: 'false',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -106,61 +116,80 @@ function whitelistRedact(whitelist, str) {
|
||||||
return '<redacted>';
|
return '<redacted>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const UID_KEY = "mx_Riot_Analytics_uid";
|
||||||
|
const CREATION_TS_KEY = "mx_Riot_Analytics_cts";
|
||||||
|
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
|
||||||
|
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
|
||||||
|
|
||||||
|
function getUid() {
|
||||||
|
try {
|
||||||
|
let data = localStorage.getItem(UID_KEY);
|
||||||
|
if (!data) {
|
||||||
|
localStorage.setItem(UID_KEY, data = [...Array(16)].map(() => Math.random().toString(16)[2]).join(''));
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Analytics error: ", e);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const HEARTBEAT_INTERVAL = 30 * 1000; // seconds
|
||||||
|
|
||||||
class Analytics {
|
class Analytics {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._paq = null;
|
this.baseUrl = null;
|
||||||
this.disabled = true;
|
this.siteId = null;
|
||||||
|
this.visitVariables = {};
|
||||||
|
|
||||||
this.firstPage = true;
|
this.firstPage = true;
|
||||||
|
this._heartbeatIntervalID = null;
|
||||||
|
|
||||||
|
this.creationTs = localStorage.getItem(CREATION_TS_KEY);
|
||||||
|
if (!this.creationTs) {
|
||||||
|
localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.lastVisitTs = localStorage.getItem(LAST_VISIT_TS_KEY);
|
||||||
|
this.visitCount = localStorage.getItem(VISIT_COUNT_KEY) || 0;
|
||||||
|
localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
get disabled() {
|
||||||
|
return !this.baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enable Analytics if initialized but disabled
|
* Enable Analytics if initialized but disabled
|
||||||
* otherwise try and initalize, no-op if piwik config missing
|
* otherwise try and initalize, no-op if piwik config missing
|
||||||
*/
|
*/
|
||||||
enable() {
|
async enable() {
|
||||||
if (this._paq || this._init()) {
|
if (!this.disabled) return;
|
||||||
this.disabled = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disable Analytics calls, will not fully unload Piwik until a refresh,
|
|
||||||
* but this is second best, Piwik should not pull anything implicitly.
|
|
||||||
*/
|
|
||||||
disable() {
|
|
||||||
this.trackEvent('Analytics', 'opt-out');
|
|
||||||
// disableHeartBeatTimer is undocumented but exists in the piwik code
|
|
||||||
// the _paq.push method will result in an error being printed in the console
|
|
||||||
// if an unknown method signature is passed
|
|
||||||
this._paq.push(['disableHeartBeatTimer']);
|
|
||||||
this.disabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_init() {
|
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
|
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
|
||||||
|
|
||||||
const url = config.piwik.url;
|
this.baseUrl = new URL("piwik.php", config.piwik.url);
|
||||||
const siteId = config.piwik.siteId;
|
// set constants
|
||||||
const self = this;
|
this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking
|
||||||
|
this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking
|
||||||
window._paq = this._paq = window._paq || [];
|
this.baseUrl.searchParams.set("apiv", 1); // API version to use
|
||||||
|
this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF
|
||||||
this._paq.push(['setTrackerUrl', url+'piwik.php']);
|
// set user parameters
|
||||||
this._paq.push(['setSiteId', siteId]);
|
this.baseUrl.searchParams.set("_id", getUid()); // uuid
|
||||||
|
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts
|
||||||
this._paq.push(['trackAllContentImpressions']);
|
this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count
|
||||||
this._paq.push(['discardHashTag', false]);
|
if (this.lastVisitTs) {
|
||||||
this._paq.push(['enableHeartBeatTimer']);
|
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
|
||||||
// this._paq.push(['enableLinkTracking', true]);
|
}
|
||||||
|
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||||
platform.getAppVersion().then((version) => {
|
try {
|
||||||
this._setVisitVariable('App Version', version);
|
this._setVisitVariable('App Version', await platform.getAppVersion());
|
||||||
}).catch(() => {
|
} catch (e) {
|
||||||
this._setVisitVariable('App Version', 'unknown');
|
this._setVisitVariable('App Version', 'unknown');
|
||||||
});
|
}
|
||||||
|
|
||||||
this._setVisitVariable('Chosen Language', getCurrentLanguage());
|
this._setVisitVariable('Chosen Language', getCurrentLanguage());
|
||||||
|
|
||||||
|
@ -168,20 +197,78 @@ class Analytics {
|
||||||
this._setVisitVariable('Instance', window.location.pathname);
|
this._setVisitVariable('Instance', window.location.pathname);
|
||||||
}
|
}
|
||||||
|
|
||||||
(function() {
|
let installedPWA = "unknown";
|
||||||
const g = document.createElement('script');
|
try {
|
||||||
const s = document.getElementsByTagName('script')[0];
|
// Known to work at least for desktop Chrome
|
||||||
g.type='text/javascript'; g.async=true; g.defer=true; g.src=url+'piwik.js';
|
installedPWA = window.matchMedia('(display-mode: standalone)').matches;
|
||||||
|
} catch (e) { }
|
||||||
|
this._setVisitVariable('Installed PWA', installedPWA);
|
||||||
|
|
||||||
g.onload = function() {
|
let touchInput = "unknown";
|
||||||
console.log('Initialised anonymous analytics');
|
try {
|
||||||
self._paq = window._paq;
|
// MDN claims broad support across browsers
|
||||||
};
|
touchInput = window.matchMedia('(pointer: coarse)').matches;
|
||||||
|
} catch (e) { }
|
||||||
|
this._setVisitVariable('Touch Input', touchInput);
|
||||||
|
|
||||||
s.parentNode.insertBefore(g, s);
|
// start heartbeat
|
||||||
})();
|
this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
/**
|
||||||
|
* Disable Analytics, stop the heartbeat and clear identifiers from localStorage
|
||||||
|
*/
|
||||||
|
disable() {
|
||||||
|
if (this.disabled) return;
|
||||||
|
this.trackEvent('Analytics', 'opt-out');
|
||||||
|
window.clearInterval(this._heartbeatIntervalID);
|
||||||
|
this.baseUrl = null;
|
||||||
|
this.visitVariables = {};
|
||||||
|
localStorage.removeItem(UID_KEY);
|
||||||
|
localStorage.removeItem(CREATION_TS_KEY);
|
||||||
|
localStorage.removeItem(VISIT_COUNT_KEY);
|
||||||
|
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _track(data) {
|
||||||
|
if (this.disabled) return;
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const params = {
|
||||||
|
...data,
|
||||||
|
url: getRedactedUrl(),
|
||||||
|
|
||||||
|
_cvar: JSON.stringify(this.visitVariables), // user custom vars
|
||||||
|
res: `${window.screen.width}x${window.screen.height}`, // resolution as WWWWxHHHH
|
||||||
|
rand: String(Math.random()).slice(2, 8), // random nonce to cache-bust
|
||||||
|
h: now.getHours(),
|
||||||
|
m: now.getMinutes(),
|
||||||
|
s: now.getSeconds(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const url = new URL(this.baseUrl);
|
||||||
|
for (const key in params) {
|
||||||
|
url.searchParams.set(key, params[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await window.fetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
mode: "no-cors",
|
||||||
|
cache: "no-cache",
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Analytics error: ", e);
|
||||||
|
window.err = e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ping() {
|
||||||
|
this._track({
|
||||||
|
ping: 1,
|
||||||
|
});
|
||||||
|
localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts
|
||||||
}
|
}
|
||||||
|
|
||||||
trackPageChange(generationTimeMs) {
|
trackPageChange(generationTimeMs) {
|
||||||
|
@ -193,31 +280,29 @@ class Analytics {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof generationTimeMs === 'number') {
|
if (typeof generationTimeMs !== 'number') {
|
||||||
this._paq.push(['setGenerationTimeMs', generationTimeMs]);
|
|
||||||
} else {
|
|
||||||
console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number');
|
console.warn('Analytics.trackPageChange: expected generationTimeMs to be a number');
|
||||||
// But continue anyway because we still want to track the change
|
// But continue anyway because we still want to track the change
|
||||||
}
|
}
|
||||||
|
|
||||||
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
this._track({
|
||||||
this._paq.push(['trackPageView']);
|
gt_ms: generationTimeMs,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
trackEvent(category, action, name, value) {
|
trackEvent(category, action, name, value) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._paq.push(['setCustomUrl', getRedactedUrl()]);
|
this._track({
|
||||||
this._paq.push(['trackEvent', category, action, name, value]);
|
e_c: category,
|
||||||
}
|
e_a: action,
|
||||||
|
e_n: name,
|
||||||
logout() {
|
e_v: value,
|
||||||
if (this.disabled) return;
|
});
|
||||||
this._paq.push(['deleteCookies']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_setVisitVariable(key, value) {
|
_setVisitVariable(key, value) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._paq.push(['setCustomVariable', customVariables[key].id, key, value, 'visit']);
|
this.visitVariables[customVariables[key].id] = [key, value];
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||||
|
@ -227,16 +312,9 @@ class Analytics {
|
||||||
if (!config.piwik) return;
|
if (!config.piwik) return;
|
||||||
|
|
||||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
||||||
const whitelistedISUrls = config.piwik.whitelistedISUrls || [];
|
|
||||||
|
|
||||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
|
|
||||||
}
|
|
||||||
|
|
||||||
setRichtextMode(state) {
|
|
||||||
if (this.disabled) return;
|
|
||||||
this._setVisitVariable('RTE: Uses Richtext Mode', state ? 'on' : 'off');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setBreadcrumbs(state) {
|
setBreadcrumbs(state) {
|
||||||
|
@ -244,13 +322,11 @@ class Analytics {
|
||||||
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetailsModal() {
|
showDetailsModal = () => {
|
||||||
let rows = [];
|
let rows = [];
|
||||||
if (window.Piwik) {
|
if (!this.disabled) {
|
||||||
const Tracker = window.Piwik.getAsyncTracker();
|
rows = Object.values(this.visitVariables);
|
||||||
rows = Object.values(customVariables).map((v) => Tracker.getCustomVariable(v.id)).filter(Boolean);
|
|
||||||
} else {
|
} else {
|
||||||
// Piwik may not have been enabled, so show example values
|
|
||||||
rows = Object.keys(customVariables).map(
|
rows = Object.keys(customVariables).map(
|
||||||
(k) => [
|
(k) => [
|
||||||
k,
|
k,
|
||||||
|
@ -271,7 +347,7 @@ class Analytics {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{ expl: _td('Your User Agent'), value: navigator.userAgent },
|
{ expl: _td('Your user agent'), value: navigator.userAgent },
|
||||||
{ expl: _td('Your device resolution'), value: resolution },
|
{ expl: _td('Your device resolution'), value: resolution },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -280,7 +356,7 @@ class Analytics {
|
||||||
title: _t('Analytics'),
|
title: _t('Analytics'),
|
||||||
description: <div className="mx_AnalyticsModal">
|
description: <div className="mx_AnalyticsModal">
|
||||||
<div>
|
<div>
|
||||||
{ _t('The information being sent to us to help make Riot.im better includes:') }
|
{ _t('The information being sent to us to help make Riot better includes:') }
|
||||||
</div>
|
</div>
|
||||||
<table>
|
<table>
|
||||||
{ rows.map((row) => <tr key={row[0]}>
|
{ rows.map((row) => <tr key={row[0]}>
|
||||||
|
@ -300,7 +376,7 @@ class Analytics {
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.mxAnalytics) {
|
if (!global.mxAnalytics) {
|
||||||
|
|
|
@ -102,6 +102,8 @@ export function getInitialLetter(name) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function avatarUrlForRoom(room, width, height, resizeMethod) {
|
export function avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||||
|
if (!room) return null; // null-guard
|
||||||
|
|
||||||
const explicitRoomAvatar = room.getAvatarUrl(
|
const explicitRoomAvatar = room.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
width,
|
width,
|
||||||
|
|
|
@ -43,7 +43,28 @@ export class AccessCancelledError extends Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getSecretStorageKey({ keys: keyInfos }) {
|
async function confirmToDismiss(name) {
|
||||||
|
let description;
|
||||||
|
if (name === "m.cross_signing.user_signing") {
|
||||||
|
description = _t("If you cancel now, you won't complete verifying the other user.");
|
||||||
|
} else if (name === "m.cross_signing.self_signing") {
|
||||||
|
description = _t("If you cancel now, you won't complete verifying your other session.");
|
||||||
|
} else {
|
||||||
|
description = _t("If you cancel now, you won't complete your secret storage operation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
|
const [sure] = await Modal.createDialog(QuestionDialog, {
|
||||||
|
title: _t("Cancel entering passphrase?"),
|
||||||
|
description,
|
||||||
|
danger: true,
|
||||||
|
cancelButton: _t("Enter passphrase"),
|
||||||
|
button: _t("Cancel"),
|
||||||
|
}).finished;
|
||||||
|
return sure;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||||
const keyInfoEntries = Object.entries(keyInfos);
|
const keyInfoEntries = Object.entries(keyInfos);
|
||||||
if (keyInfoEntries.length > 1) {
|
if (keyInfoEntries.length > 1) {
|
||||||
throw new Error("Multiple storage key requests not implemented");
|
throw new Error("Multiple storage key requests not implemented");
|
||||||
|
@ -70,6 +91,7 @@ async function getSecretStorageKey({ keys: keyInfos }) {
|
||||||
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
|
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
|
||||||
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
||||||
AccessSecretStorageDialog,
|
AccessSecretStorageDialog,
|
||||||
|
/* props= */
|
||||||
{
|
{
|
||||||
keyInfo: info,
|
keyInfo: info,
|
||||||
checkPrivateKey: async (input) => {
|
checkPrivateKey: async (input) => {
|
||||||
|
@ -77,6 +99,17 @@ async function getSecretStorageKey({ keys: keyInfos }) {
|
||||||
return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey);
|
return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
/* className= */ null,
|
||||||
|
/* isPriorityModal= */ false,
|
||||||
|
/* isStaticModal= */ false,
|
||||||
|
/* options= */ {
|
||||||
|
onBeforeClose: async (reason) => {
|
||||||
|
if (reason === "backgroundClick") {
|
||||||
|
return confirmToDismiss(ssssItemName);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const [input] = await finished;
|
const [input] = await finished;
|
||||||
if (!input) {
|
if (!input) {
|
||||||
|
@ -115,18 +148,21 @@ export const crossSigningCallbacks = {
|
||||||
*
|
*
|
||||||
* @param {Function} [func] An operation to perform once secret storage has been
|
* @param {Function} [func] An operation to perform once secret storage has been
|
||||||
* bootstrapped. Optional.
|
* bootstrapped. Optional.
|
||||||
|
* @param {bool} [force] Reset secret storage even if it's already set up
|
||||||
*/
|
*/
|
||||||
export async function accessSecretStorage(func = async () => { }) {
|
export async function accessSecretStorage(func = async () => { }, force = false) {
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
secretStorageBeingAccessed = true;
|
secretStorageBeingAccessed = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!await cli.hasSecretStorageKey()) {
|
if (!await cli.hasSecretStorageKey() || force) {
|
||||||
// This dialog calls bootstrap itself after guiding the user through
|
// This dialog calls bootstrap itself after guiding the user through
|
||||||
// passphrase creation.
|
// passphrase creation.
|
||||||
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
||||||
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
|
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
|
||||||
null, null, /* priority = */ false, /* static = */ true,
|
{
|
||||||
|
force,
|
||||||
|
},
|
||||||
|
null, /* priority = */ false, /* static = */ true,
|
||||||
);
|
);
|
||||||
const [confirmed] = await finished;
|
const [confirmed] = await finished;
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
|
|
|
@ -160,7 +160,7 @@ const transformTags = { // custom to matrix
|
||||||
delete attribs.target;
|
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 };
|
return { tagName, attribs };
|
||||||
},
|
},
|
||||||
'img': function(tagName, attribs) {
|
'img': function(tagName, attribs) {
|
||||||
|
|
|
@ -435,7 +435,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl, credentials.identityServerUrl);
|
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||||
|
|
||||||
if (localStorage) {
|
if (localStorage) {
|
||||||
try {
|
try {
|
||||||
|
@ -632,7 +632,7 @@ export async function onLoggedOut() {
|
||||||
* @returns {Promise} promise which resolves once the stores have been cleared
|
* @returns {Promise} promise which resolves once the stores have been cleared
|
||||||
*/
|
*/
|
||||||
async function _clearStorage() {
|
async function _clearStorage() {
|
||||||
Analytics.logout();
|
Analytics.disable();
|
||||||
|
|
||||||
if (window.localStorage) {
|
if (window.localStorage) {
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default class Markdown {
|
||||||
// thus opening in a new tab.
|
// thus opening in a new tab.
|
||||||
if (externalLinks) {
|
if (externalLinks) {
|
||||||
attrs.push(['target', '_blank']);
|
attrs.push(['target', '_blank']);
|
||||||
attrs.push(['rel', 'noopener']);
|
attrs.push(['rel', 'noreferrer noopener']);
|
||||||
}
|
}
|
||||||
this.tag('a', attrs);
|
this.tag('a', attrs);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,7 +32,7 @@ import MatrixClientBackedSettingsHandler from "./settings/handlers/MatrixClientB
|
||||||
import * as StorageManager from './utils/StorageManager';
|
import * as StorageManager from './utils/StorageManager';
|
||||||
import IdentityAuthClient from './IdentityAuthClient';
|
import IdentityAuthClient from './IdentityAuthClient';
|
||||||
import { crossSigningCallbacks } from './CrossSigningManager';
|
import { crossSigningCallbacks } from './CrossSigningManager';
|
||||||
import {SCAN_QR_CODE_METHOD, SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
|
|
||||||
interface MatrixClientCreds {
|
interface MatrixClientCreds {
|
||||||
homeserverUrl: string,
|
homeserverUrl: string,
|
||||||
|
@ -221,7 +221,6 @@ class _MatrixClientPeg {
|
||||||
verificationMethods: [
|
verificationMethods: [
|
||||||
verificationMethods.SAS,
|
verificationMethods.SAS,
|
||||||
SHOW_QR_CODE_METHOD,
|
SHOW_QR_CODE_METHOD,
|
||||||
SCAN_QR_CODE_METHOD, // XXX: We don't actually support scanning yet!
|
|
||||||
verificationMethods.RECIPROCATE_QR_CODE,
|
verificationMethods.RECIPROCATE_QR_CODE,
|
||||||
],
|
],
|
||||||
unstableClientRelationAggregation: true,
|
unstableClientRelationAggregation: true,
|
||||||
|
|
73
src/Modal.js
73
src/Modal.js
|
@ -47,7 +47,7 @@ class ModalManager {
|
||||||
} */
|
} */
|
||||||
];
|
];
|
||||||
|
|
||||||
this.closeAll = this.closeAll.bind(this);
|
this.onBackgroundClick = this.onBackgroundClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasDialogs() {
|
hasDialogs() {
|
||||||
|
@ -106,7 +106,7 @@ class ModalManager {
|
||||||
return this.appendDialogAsync(...rest);
|
return this.appendDialogAsync(...rest);
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildModal(prom, props, className) {
|
_buildModal(prom, props, className, options) {
|
||||||
const modal = {};
|
const modal = {};
|
||||||
|
|
||||||
// never call this from onFinished() otherwise it will loop
|
// never call this from onFinished() otherwise it will loop
|
||||||
|
@ -124,13 +124,27 @@ class ModalManager {
|
||||||
);
|
);
|
||||||
modal.onFinished = props ? props.onFinished : null;
|
modal.onFinished = props ? props.onFinished : null;
|
||||||
modal.className = className;
|
modal.className = className;
|
||||||
|
modal.onBeforeClose = options.onBeforeClose;
|
||||||
|
modal.beforeClosePromise = null;
|
||||||
|
modal.close = closeDialog;
|
||||||
|
modal.closeReason = null;
|
||||||
|
|
||||||
return {modal, closeDialog, onFinishedProm};
|
return {modal, closeDialog, onFinishedProm};
|
||||||
}
|
}
|
||||||
|
|
||||||
_getCloseFn(modal, props) {
|
_getCloseFn(modal, props) {
|
||||||
const deferred = defer();
|
const deferred = defer();
|
||||||
return [(...args) => {
|
return [async (...args) => {
|
||||||
|
if (modal.beforeClosePromise) {
|
||||||
|
await modal.beforeClosePromise;
|
||||||
|
} else if (modal.onBeforeClose) {
|
||||||
|
modal.beforeClosePromise = modal.onBeforeClose(modal.closeReason);
|
||||||
|
const shouldClose = await modal.beforeClosePromise;
|
||||||
|
modal.beforeClosePromise = null;
|
||||||
|
if (!shouldClose) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
deferred.resolve(args);
|
deferred.resolve(args);
|
||||||
if (props && props.onFinished) props.onFinished.apply(null, args);
|
if (props && props.onFinished) props.onFinished.apply(null, args);
|
||||||
const i = this._modals.indexOf(modal);
|
const i = this._modals.indexOf(modal);
|
||||||
|
@ -156,6 +170,12 @@ class ModalManager {
|
||||||
}, deferred.promise];
|
}, deferred.promise];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @callback onBeforeClose
|
||||||
|
* @param {string?} reason either "backgroundClick" or null
|
||||||
|
* @return {Promise<bool>} whether the dialog should close
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a modal view.
|
* Open a modal view.
|
||||||
*
|
*
|
||||||
|
@ -183,11 +203,12 @@ class ModalManager {
|
||||||
* also be removed from the stack. This is not compatible
|
* also be removed from the stack. This is not compatible
|
||||||
* with being a priority modal. Only one modal can be
|
* with being a priority modal. Only one modal can be
|
||||||
* static at a time.
|
* static at a time.
|
||||||
|
* @param {Object} options? extra options for the dialog
|
||||||
|
* @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog
|
||||||
* @returns {object} Object with 'close' parameter being a function that will close the dialog
|
* @returns {object} Object with 'close' parameter being a function that will close the dialog
|
||||||
*/
|
*/
|
||||||
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal) {
|
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) {
|
||||||
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className);
|
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options);
|
||||||
|
|
||||||
if (isPriorityModal) {
|
if (isPriorityModal) {
|
||||||
// XXX: This is destructive
|
// XXX: This is destructive
|
||||||
this._priorityModal = modal;
|
this._priorityModal = modal;
|
||||||
|
@ -206,7 +227,7 @@ class ModalManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
appendDialogAsync(prom, props, className) {
|
appendDialogAsync(prom, props, className) {
|
||||||
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className);
|
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {});
|
||||||
|
|
||||||
this._modals.push(modal);
|
this._modals.push(modal);
|
||||||
this._reRender();
|
this._reRender();
|
||||||
|
@ -216,24 +237,22 @@ class ModalManager {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
closeAll() {
|
onBackgroundClick() {
|
||||||
const modalsToClose = [...this._modals, this._priorityModal];
|
const modal = this._getCurrentModal();
|
||||||
this._modals = [];
|
if (!modal) {
|
||||||
this._priorityModal = null;
|
return;
|
||||||
|
|
||||||
if (this._staticModal && modalsToClose.length === 0) {
|
|
||||||
modalsToClose.push(this._staticModal);
|
|
||||||
this._staticModal = null;
|
|
||||||
}
|
}
|
||||||
|
// we want to pass a reason to the onBeforeClose
|
||||||
|
// callback, but close is currently defined to
|
||||||
|
// pass all number of arguments to the onFinished callback
|
||||||
|
// so, pass the reason to close through a member variable
|
||||||
|
modal.closeReason = "backgroundClick";
|
||||||
|
modal.close();
|
||||||
|
modal.closeReason = null;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0; i < modalsToClose.length; i++) {
|
_getCurrentModal() {
|
||||||
const m = modalsToClose[i];
|
return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal);
|
||||||
if (m && m.onFinished) {
|
|
||||||
m.onFinished(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this._reRender();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_reRender() {
|
_reRender() {
|
||||||
|
@ -264,7 +283,7 @@ class ModalManager {
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{ this._staticModal.elem }
|
{ this._staticModal.elem }
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.closeAll}></div>
|
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -274,8 +293,8 @@ class ModalManager {
|
||||||
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
|
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = this._priorityModal ? this._priorityModal : this._modals[0];
|
const modal = this._getCurrentModal();
|
||||||
if (modal) {
|
if (modal !== this._staticModal) {
|
||||||
const classes = "mx_Dialog_wrapper "
|
const classes = "mx_Dialog_wrapper "
|
||||||
+ (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '')
|
+ (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '')
|
||||||
+ (modal.className ? modal.className : '');
|
+ (modal.className ? modal.className : '');
|
||||||
|
@ -285,7 +304,7 @@ class ModalManager {
|
||||||
<div className="mx_Dialog">
|
<div className="mx_Dialog">
|
||||||
{modal.elem}
|
{modal.elem}
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_Dialog_background" onClick={this.closeAll}></div>
|
<div className="mx_Dialog_background" onClick={this.onBackgroundClick}></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -153,10 +153,12 @@ const Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
start: function() {
|
start: function() {
|
||||||
this.boundOnEvent = this.onEvent.bind(this);
|
// do not re-bind in the case of repeated call
|
||||||
this.boundOnSyncStateChange = this.onSyncStateChange.bind(this);
|
this.boundOnEvent = this.boundOnEvent || this.onEvent.bind(this);
|
||||||
this.boundOnRoomReceipt = this.onRoomReceipt.bind(this);
|
this.boundOnSyncStateChange = this.boundOnSyncStateChange || this.onSyncStateChange.bind(this);
|
||||||
this.boundOnEventDecrypted = this.onEventDecrypted.bind(this);
|
this.boundOnRoomReceipt = this.boundOnRoomReceipt || this.onRoomReceipt.bind(this);
|
||||||
|
this.boundOnEventDecrypted = this.boundOnEventDecrypted || this.onEventDecrypted.bind(this);
|
||||||
|
|
||||||
MatrixClientPeg.get().on('event', this.boundOnEvent);
|
MatrixClientPeg.get().on('event', this.boundOnEvent);
|
||||||
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
MatrixClientPeg.get().on('Room.receipt', this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted);
|
MatrixClientPeg.get().on('Event.decrypted', this.boundOnEventDecrypted);
|
||||||
|
@ -166,7 +168,7 @@ const Notifier = {
|
||||||
},
|
},
|
||||||
|
|
||||||
stop: function() {
|
stop: function() {
|
||||||
if (MatrixClientPeg.get() && this.boundOnRoomTimeline) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('Event', this.boundOnEvent);
|
MatrixClientPeg.get().removeListener('Event', this.boundOnEvent);
|
||||||
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
MatrixClientPeg.get().removeListener('Room.receipt', this.boundOnRoomReceipt);
|
||||||
MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted);
|
MatrixClientPeg.get().removeListener('Event.decrypted', this.boundOnEventDecrypted);
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
* of aliases. Otherwise return null;
|
* of aliases. Otherwise return null;
|
||||||
*/
|
*/
|
||||||
export function getDisplayAliasForRoom(room) {
|
export function getDisplayAliasForRoom(room) {
|
||||||
return room.getCanonicalAlias() || room.getAliases()[0];
|
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -46,9 +46,18 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCurrentRoom(room) {
|
updateCurrentRoom = async (room) => {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
const stats = await eventIndex.getStats();
|
let stats;
|
||||||
|
|
||||||
|
try {
|
||||||
|
stats = await eventIndex.getStats();
|
||||||
|
} catch {
|
||||||
|
// This call may fail if sporadically, not a huge issue as we will
|
||||||
|
// try later again and probably succeed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let currentRoom = null;
|
let currentRoom = null;
|
||||||
|
|
||||||
if (room) currentRoom = room.name;
|
if (room) currentRoom = room.name;
|
||||||
|
@ -63,13 +72,13 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||||
roomCount: roomCount,
|
roomCount: roomCount,
|
||||||
currentRoom: currentRoom,
|
currentRoom: currentRoom,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
if (eventIndex !== null) {
|
if (eventIndex !== null) {
|
||||||
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,14 +92,21 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
if (eventIndex !== null) {
|
if (eventIndex !== null) {
|
||||||
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
eventIndex.on("changedCheckpoint", this.updateCurrentRoom);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const stats = await eventIndex.getStats();
|
||||||
|
eventIndexSize = stats.size;
|
||||||
|
eventCount = stats.eventCount;
|
||||||
|
} catch {
|
||||||
|
// This call may fail if sporadically, not a huge issue as we
|
||||||
|
// will try later again in the updateCurrentRoom call and
|
||||||
|
// probably succeed.
|
||||||
|
}
|
||||||
|
|
||||||
const stats = await eventIndex.getStats();
|
|
||||||
const roomStats = eventIndex.crawlingRooms();
|
const roomStats = eventIndex.crawlingRooms();
|
||||||
eventIndexSize = stats.size;
|
|
||||||
crawlingRoomsCount = roomStats.crawlingRooms.size;
|
crawlingRoomsCount = roomStats.crawlingRooms.size;
|
||||||
roomCount = roomStats.totalRooms.size;
|
roomCount = roomStats.totalRooms.size;
|
||||||
eventCount = stats.eventCount;
|
|
||||||
|
|
||||||
const room = eventIndex.currentRoom();
|
const room = eventIndex.currentRoom();
|
||||||
if (room) currentRoom = room.name;
|
if (room) currentRoom = room.name;
|
||||||
|
@ -144,8 +160,10 @@ export default class ManageEventIndexDialog extends React.Component {
|
||||||
<div className='mx_SettingsTab_subsectionText'>
|
<div className='mx_SettingsTab_subsectionText'>
|
||||||
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
|
||||||
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
|
||||||
{_t("Number of rooms:")} {formatCountLong(this.state.crawlingRoomsCount)} {_t("of ")}
|
{_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", {
|
||||||
{formatCountLong(this.state.roomCount)}<br />
|
crawlingRooms: formatCountLong(this.state.crawlingRoomsCount),
|
||||||
|
totalRooms: formatCountLong(this.state.roomCount),
|
||||||
|
})} <br />
|
||||||
{crawlerState}<br />
|
{crawlerState}<br />
|
||||||
<Field
|
<Field
|
||||||
id={"crawlerSleepTimeMs"}
|
id={"crawlerSleepTimeMs"}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
import SettingsStore from '../../../../settings/SettingsStore';
|
import SettingsStore from '../../../../settings/SettingsStore';
|
||||||
|
import AccessibleButton from "../../../../components/views/elements/AccessibleButton";
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||||
|
@ -191,44 +192,38 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseNextClick = () => {
|
_onPassPhraseNextClick = async (e) => {
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
e.preventDefault();
|
||||||
}
|
|
||||||
|
|
||||||
_onPassPhraseKeyPress = async (e) => {
|
// If we're waiting for the timeout before updating the result at this point,
|
||||||
if (e.key === 'Enter') {
|
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
||||||
// If we're waiting for the timeout before updating the result at this point,
|
// even if the user entered a valid passphrase
|
||||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
// even if the user entered a valid passphrase
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
this._setZxcvbnResultTimeout = null;
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
await new Promise((resolve) => {
|
||||||
this._setZxcvbnResultTimeout = null;
|
this.setState({
|
||||||
await new Promise((resolve) => {
|
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||||
this.setState({
|
}, resolve);
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
});
|
||||||
}, resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this._passPhraseIsValid()) {
|
|
||||||
this._onPassPhraseNextClick();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (this._passPhraseIsValid()) {
|
||||||
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onPassPhraseConfirmNextClick = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick = async () => {
|
|
||||||
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
this._keyBackupInfo = await MatrixClientPeg.get().prepareKeyBackupVersion(this.state.passPhrase);
|
||||||
this.setState({
|
this.setState({
|
||||||
copied: false,
|
copied: false,
|
||||||
downloaded: false,
|
downloaded: false,
|
||||||
phase: PHASE_SHOWKEY,
|
phase: PHASE_SHOWKEY,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
_onPassPhraseConfirmKeyPress = (e) => {
|
|
||||||
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
|
|
||||||
this._onPassPhraseConfirmNextClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSetAgainClick = () => {
|
_onSetAgainClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -299,7 +294,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
|
||||||
{ b: sub => <b>{sub}</b> },
|
{ b: sub => <b>{sub}</b> },
|
||||||
|
@ -314,7 +309,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
|
||||||
<input type="password"
|
<input type="password"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
onKeyPress={this._onPassPhraseKeyPress}
|
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||||
placeholder={_t("Enter a passphrase...")}
|
placeholder={_t("Enter a passphrase...")}
|
||||||
|
@ -327,7 +321,8 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={!this._passPhraseIsValid()}
|
disabled={!this._passPhraseIsValid()}
|
||||||
|
@ -335,11 +330,11 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>{_t("Advanced")}</summary>
|
<summary>{_t("Advanced")}</summary>
|
||||||
<p><button onClick={this._onSkipPassPhraseClick} >
|
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
|
||||||
{_t("Set up with a recovery key")}
|
{_t("Set up with a recovery key")}
|
||||||
</button></p>
|
</AccessibleButton>
|
||||||
</details>
|
</details>
|
||||||
</div>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderPhasePassPhraseConfirm() {
|
_renderPhasePassPhraseConfirm() {
|
||||||
|
@ -371,7 +366,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Please enter your passphrase a second time to confirm.",
|
"Please enter your passphrase a second time to confirm.",
|
||||||
)}</p>
|
)}</p>
|
||||||
|
@ -380,7 +375,6 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
<div>
|
<div>
|
||||||
<input type="password"
|
<input type="password"
|
||||||
onChange={this._onPassPhraseConfirmChange}
|
onChange={this._onPassPhraseConfirmChange}
|
||||||
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
|
||||||
value={this.state.passPhraseConfirm}
|
value={this.state.passPhraseConfirm}
|
||||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||||
placeholder={_t("Repeat your passphrase...")}
|
placeholder={_t("Repeat your passphrase...")}
|
||||||
|
@ -390,12 +384,13 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
|
||||||
{passPhraseMatch}
|
{passPhraseMatch}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||||
/>
|
/>
|
||||||
</div>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderPhaseShowKey() {
|
_renderPhaseShowKey() {
|
||||||
|
|
|
@ -55,10 +55,12 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
hasCancel: PropTypes.bool,
|
hasCancel: PropTypes.bool,
|
||||||
accountPassword: PropTypes.string,
|
accountPassword: PropTypes.string,
|
||||||
|
force: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
hasCancel: true,
|
hasCancel: true,
|
||||||
|
force: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -107,7 +109,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
|
MatrixClientPeg.get().isCryptoEnabled() && await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo)
|
||||||
);
|
);
|
||||||
|
|
||||||
const phase = backupInfo ? PHASE_MIGRATE : PHASE_PASSPHRASE;
|
const { force } = this.props;
|
||||||
|
const phase = (backupInfo && !force) ? PHASE_MIGRATE : PHASE_PASSPHRASE;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
phase,
|
phase,
|
||||||
|
@ -219,13 +222,24 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
const cli = MatrixClientPeg.get();
|
||||||
|
|
||||||
|
const { force } = this.props;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cli.bootstrapSecretStorage({
|
if (force) {
|
||||||
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
await cli.bootstrapSecretStorage({
|
||||||
createSecretStorageKey: async () => this._keyInfo,
|
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||||
keyBackupInfo: this.state.backupInfo,
|
createSecretStorageKey: async () => this._keyInfo,
|
||||||
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
|
setupNewKeyBackup: true,
|
||||||
});
|
setupNewSecretStorage: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await cli.bootstrapSecretStorage({
|
||||||
|
authUploadDeviceSigningKeys: this._doBootstrapUIAuth,
|
||||||
|
createSecretStorageKey: async () => this._keyInfo,
|
||||||
|
keyBackupInfo: this.state.backupInfo,
|
||||||
|
setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup,
|
||||||
|
});
|
||||||
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
});
|
});
|
||||||
|
@ -289,31 +303,31 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseNextClick = () => {
|
_onPassPhraseNextClick = async (e) => {
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
e.preventDefault();
|
||||||
}
|
|
||||||
|
|
||||||
_onPassPhraseKeyPress = async (e) => {
|
// If we're waiting for the timeout before updating the result at this point,
|
||||||
if (e.key === 'Enter') {
|
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
||||||
// If we're waiting for the timeout before updating the result at this point,
|
// even if the user entered a valid passphrase
|
||||||
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
// even if the user entered a valid passphrase
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
if (this._setZxcvbnResultTimeout !== null) {
|
this._setZxcvbnResultTimeout = null;
|
||||||
clearTimeout(this._setZxcvbnResultTimeout);
|
await new Promise((resolve) => {
|
||||||
this._setZxcvbnResultTimeout = null;
|
this.setState({
|
||||||
await new Promise((resolve) => {
|
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||||
this.setState({
|
}, resolve);
|
||||||
zxcvbnResult: scorePassword(this.state.passPhrase),
|
});
|
||||||
}, resolve);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this._passPhraseIsValid()) {
|
|
||||||
this._onPassPhraseNextClick();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
if (this._passPhraseIsValid()) {
|
||||||
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_onPassPhraseConfirmNextClick = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.state.passPhrase !== this.state.passPhraseConfirm) return;
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick = async () => {
|
|
||||||
const [keyInfo, encodedRecoveryKey] =
|
const [keyInfo, encodedRecoveryKey] =
|
||||||
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
await MatrixClientPeg.get().createRecoveryKeyFromPassphrase(this.state.passPhrase);
|
||||||
this._keyInfo = keyInfo;
|
this._keyInfo = keyInfo;
|
||||||
|
@ -325,12 +339,6 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseConfirmKeyPress = (e) => {
|
|
||||||
if (e.key === 'Enter' && this.state.passPhrase === this.state.passPhraseConfirm) {
|
|
||||||
this._onPassPhraseConfirmNextClick();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onSetAgainClick = () => {
|
_onSetAgainClick = () => {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
|
@ -400,7 +408,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
} else if (this.state.canUploadKeysWithPasswordOnly) {
|
} else if (this.state.canUploadKeysWithPasswordOnly) {
|
||||||
authPrompt = <div>
|
authPrompt = <div>
|
||||||
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
|
||||||
<div><Field type="password"
|
<div><Field
|
||||||
|
type="password"
|
||||||
id="mx_CreateSecretStorage_accountPassword"
|
id="mx_CreateSecretStorage_accountPassword"
|
||||||
label={_t("Password")}
|
label={_t("Password")}
|
||||||
value={this.state.accountPassword}
|
value={this.state.accountPassword}
|
||||||
|
@ -422,8 +431,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
"as trusted for other users.",
|
"as trusted for other users.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div>{authPrompt}</div>
|
<div>{authPrompt}</div>
|
||||||
<DialogButtons primaryButton={nextCaption}
|
<DialogButtons
|
||||||
primaryIsSubmit={true}
|
primaryButton={nextCaption}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
|
||||||
>
|
>
|
||||||
|
@ -467,7 +476,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <div>
|
return <form onSubmit={this._onPassPhraseNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Set up encryption on this session to allow it to verify other sessions, " +
|
"Set up encryption on this session to allow it to verify other sessions, " +
|
||||||
"granting them access to encrypted messages and marking them as trusted for other users.",
|
"granting them access to encrypted messages and marking them as trusted for other users.",
|
||||||
|
@ -483,10 +492,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
id="mx_CreateSecretStorageDialog_passPhraseField"
|
id="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
onKeyPress={this._onPassPhraseKeyPress}
|
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
label={_t("Enter a passphrase")}
|
label={_t("Enter a passphrase")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
<div className="mx_CreateSecretStorageDialog_passPhraseHelp">
|
||||||
{strengthMeter}
|
{strengthMeter}
|
||||||
|
@ -499,7 +508,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
onChange={this._onUseKeyBackupChange} value={this.state.useKeyBackup}
|
onChange={this._onUseKeyBackupChange} value={this.state.useKeyBackup}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<DialogButtons primaryButton={_t('Continue')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
onPrimaryButtonClick={this._onPassPhraseNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={!this._passPhraseIsValid()}
|
disabled={!this._passPhraseIsValid()}
|
||||||
|
@ -516,7 +526,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
{_t("Set up with a recovery key")}
|
{_t("Set up with a recovery key")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</details>
|
</details>
|
||||||
</div>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderPhasePassPhraseConfirm() {
|
_renderPhasePassPhraseConfirm() {
|
||||||
|
@ -549,25 +559,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Enter your passphrase a second time to confirm it.",
|
"Enter your passphrase a second time to confirm it.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
|
||||||
<Field type="password"
|
<Field
|
||||||
|
type="password"
|
||||||
id="mx_CreateSecretStorageDialog_passPhraseField"
|
id="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
onChange={this._onPassPhraseConfirmChange}
|
onChange={this._onPassPhraseConfirmChange}
|
||||||
onKeyPress={this._onPassPhraseConfirmKeyPress}
|
|
||||||
value={this.state.passPhraseConfirm}
|
value={this.state.passPhraseConfirm}
|
||||||
className="mx_CreateSecretStorageDialog_passPhraseField"
|
className="mx_CreateSecretStorageDialog_passPhraseField"
|
||||||
label={_t("Confirm your passphrase")}
|
label={_t("Confirm your passphrase")}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
|
||||||
{passPhraseMatch}
|
{passPhraseMatch}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DialogButtons primaryButton={_t('Continue')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Continue')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
onPrimaryButtonClick={this._onPassPhraseConfirmNextClick}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
disabled={this.state.passPhrase !== this.state.passPhraseConfirm}
|
||||||
|
@ -577,7 +589,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
className="danger"
|
className="danger"
|
||||||
>{_t("Skip")}</button>
|
>{_t("Skip")}</button>
|
||||||
</DialogButtons>
|
</DialogButtons>
|
||||||
</div>;
|
</form>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderPhaseShowKey() {
|
_renderPhaseShowKey() {
|
||||||
|
|
|
@ -23,7 +23,6 @@ import AutocompleteProvider from './AutocompleteProvider';
|
||||||
import {MatrixClientPeg} from '../MatrixClientPeg';
|
import {MatrixClientPeg} from '../MatrixClientPeg';
|
||||||
import QueryMatcher from './QueryMatcher';
|
import QueryMatcher from './QueryMatcher';
|
||||||
import {PillCompletion} from './Components';
|
import {PillCompletion} from './Components';
|
||||||
import {getDisplayAliasForRoom} from '../Rooms';
|
|
||||||
import * as sdk from '../index';
|
import * as sdk from '../index';
|
||||||
import _sortBy from 'lodash/sortBy';
|
import _sortBy from 'lodash/sortBy';
|
||||||
import {makeRoomPermalink} from "../utils/permalinks/Permalinks";
|
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 {
|
export default class RoomProvider extends AutocompleteProvider {
|
||||||
constructor() {
|
constructor() {
|
||||||
super(ROOM_REGEX);
|
super(ROOM_REGEX);
|
||||||
this.matcher = new QueryMatcher([], {
|
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);
|
const {command, range} = this.getCurrentCommand(query, selection, force);
|
||||||
if (command) {
|
if (command) {
|
||||||
// the only reason we need to do this is because Fuse only matches on properties
|
// the only reason we need to do this is because Fuse only matches on properties
|
||||||
let matcherObjects = client.getVisibleRooms().filter(
|
let matcherObjects = client.getVisibleRooms().reduce((aliases, room) => {
|
||||||
(room) => !!room && !!getDisplayAliasForRoom(room),
|
if (room.getCanonicalAlias()) {
|
||||||
).map((room) => {
|
aliases = aliases.concat(matcherObject(room, room.getCanonicalAlias(), room.name));
|
||||||
return {
|
}
|
||||||
room: room,
|
if (room.getAltAliases().length) {
|
||||||
name: room.name,
|
const altAliases = room.getAltAliases().map(alias => matcherObject(room, alias));
|
||||||
displayedAlias: getDisplayAliasForRoom(room),
|
aliases = aliases.concat(altAliases);
|
||||||
};
|
}
|
||||||
});
|
return aliases;
|
||||||
|
}, []);
|
||||||
// Filter out any matches where the user will have also autocompleted new rooms
|
// Filter out any matches where the user will have also autocompleted new rooms
|
||||||
matcherObjects = matcherObjects.filter((r) => {
|
matcherObjects = matcherObjects.filter((r) => {
|
||||||
const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", "");
|
const tombstone = r.room.currentState.getStateEvents("m.room.tombstone", "");
|
||||||
|
@ -84,16 +91,16 @@ export default class RoomProvider extends AutocompleteProvider {
|
||||||
completions = _sortBy(completions, [
|
completions = _sortBy(completions, [
|
||||||
(c) => score(matchedString, c.displayedAlias),
|
(c) => score(matchedString, c.displayedAlias),
|
||||||
(c) => c.displayedAlias.length,
|
(c) => c.displayedAlias.length,
|
||||||
]).map((room) => {
|
]);
|
||||||
const displayAlias = getDisplayAliasForRoom(room.room) || room.roomId;
|
completions = completions.map((room) => {
|
||||||
return {
|
return {
|
||||||
completion: displayAlias,
|
completion: room.displayedAlias,
|
||||||
completionId: displayAlias,
|
completionId: room.room.roomId,
|
||||||
type: "room",
|
type: "room",
|
||||||
suffix: ' ',
|
suffix: ' ',
|
||||||
href: makeRoomPermalink(displayAlias),
|
href: makeRoomPermalink(room.displayedAlias),
|
||||||
component: (
|
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,
|
range,
|
||||||
};
|
};
|
||||||
|
|
|
@ -255,7 +255,7 @@ export class ContextMenu extends React.Component {
|
||||||
|
|
||||||
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
if (chevronFace === 'top' || chevronFace === 'bottom') {
|
||||||
chevronOffset.left = props.chevronOffset;
|
chevronOffset.left = props.chevronOffset;
|
||||||
} else {
|
} else if (position.top !== undefined) {
|
||||||
const target = position.top;
|
const target = position.top;
|
||||||
|
|
||||||
// By default, no adjustment is made
|
// By default, no adjustment is made
|
||||||
|
|
|
@ -95,8 +95,8 @@ const FilePanel = createReactClass({
|
||||||
// this could be made more general in the future or the filter logic
|
// this could be made more general in the future or the filter logic
|
||||||
// could be fixed.
|
// could be fixed.
|
||||||
if (EventIndexPeg.get() !== null) {
|
if (EventIndexPeg.get() !== null) {
|
||||||
client.on('Room.timeline', this.onRoomTimeline.bind(this));
|
client.on('Room.timeline', this.onRoomTimeline);
|
||||||
client.on('Event.decrypted', this.onEventDecrypted.bind(this));
|
client.on('Event.decrypted', this.onEventDecrypted);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -107,8 +107,8 @@ const FilePanel = createReactClass({
|
||||||
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
|
if (!MatrixClientPeg.get().isRoomEncrypted(this.props.roomId)) return;
|
||||||
|
|
||||||
if (EventIndexPeg.get() !== null) {
|
if (EventIndexPeg.get() !== null) {
|
||||||
client.removeListener('Room.timeline', this.onRoomTimeline.bind(this));
|
client.removeListener('Room.timeline', this.onRoomTimeline);
|
||||||
client.removeListener('Event.decrypted', this.onEventDecrypted.bind(this));
|
client.removeListener('Event.decrypted', this.onEventDecrypted);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -821,10 +821,10 @@ export default createReactClass({
|
||||||
{_t(
|
{_t(
|
||||||
"Want more than a community? <a>Get your own server</a>", {},
|
"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='' />
|
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -65,6 +65,7 @@ import { ThemeWatcher } from "../../theme";
|
||||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||||
import { defer } from "../../utils/promise";
|
import { defer } from "../../utils/promise";
|
||||||
import ToastStore from "../../stores/ToastStore";
|
import ToastStore from "../../stores/ToastStore";
|
||||||
|
import * as StorageManager from "../../utils/StorageManager";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
export const VIEWS = {
|
export const VIEWS = {
|
||||||
|
@ -1174,6 +1175,7 @@ export default createReactClass({
|
||||||
* Called when a new logged in session has started
|
* Called when a new logged in session has started
|
||||||
*/
|
*/
|
||||||
_onLoggedIn: async function() {
|
_onLoggedIn: async function() {
|
||||||
|
ThemeController.isLogin = false;
|
||||||
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
this.setStateForNewView({ view: VIEWS.LOGGED_IN });
|
||||||
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
if (MatrixClientPeg.currentUserIsJustRegistered()) {
|
||||||
MatrixClientPeg.setJustRegisteredUserId(null);
|
MatrixClientPeg.setJustRegisteredUserId(null);
|
||||||
|
@ -1193,6 +1195,8 @@ export default createReactClass({
|
||||||
} else {
|
} else {
|
||||||
this._showScreenAfterLogin();
|
this._showScreenAfterLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StorageManager.tryPersistStorage();
|
||||||
},
|
},
|
||||||
|
|
||||||
_showScreenAfterLogin: function() {
|
_showScreenAfterLogin: function() {
|
||||||
|
@ -1371,7 +1375,8 @@ export default createReactClass({
|
||||||
cancelButton: _t('Dismiss'),
|
cancelButton: _t('Dismiss'),
|
||||||
onFinished: (confirmed) => {
|
onFinished: (confirmed) => {
|
||||||
if (confirmed) {
|
if (confirmed) {
|
||||||
window.open(consentUri, '_blank');
|
const wnd = window.open(consentUri, '_blank');
|
||||||
|
wnd.opener = null;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}, null, true);
|
}, null, true);
|
||||||
|
|
|
@ -115,6 +115,7 @@ export default class MessagePanel extends React.Component {
|
||||||
// previous positions the read marker has been in, so we can
|
// previous positions the read marker has been in, so we can
|
||||||
// display 'ghost' read markers that are animating away
|
// display 'ghost' read markers that are animating away
|
||||||
ghostReadMarkers: [],
|
ghostReadMarkers: [],
|
||||||
|
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
// opaque readreceipt info for each userId; used by ReadReceiptMarker
|
||||||
|
@ -164,6 +165,9 @@ export default class MessagePanel extends React.Component {
|
||||||
this._readMarkerNode = createRef();
|
this._readMarkerNode = createRef();
|
||||||
this._whoIsTyping = createRef();
|
this._whoIsTyping = createRef();
|
||||||
this._scrollPanel = createRef();
|
this._scrollPanel = createRef();
|
||||||
|
|
||||||
|
this._showTypingNotificationsWatcherRef =
|
||||||
|
SettingsStore.watchSetting("showTypingNotifications", null, this.onShowTypingNotificationsChange);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -172,6 +176,7 @@ export default class MessagePanel extends React.Component {
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
this._isMounted = false;
|
this._isMounted = false;
|
||||||
|
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
|
@ -184,6 +189,12 @@ export default class MessagePanel extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onShowTypingNotificationsChange = () => {
|
||||||
|
this.setState({
|
||||||
|
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/* get the DOM node representing the given event */
|
/* get the DOM node representing the given event */
|
||||||
getNodeForEventId(eventId) {
|
getNodeForEventId(eventId) {
|
||||||
if (!this.eventNodes) {
|
if (!this.eventNodes) {
|
||||||
|
@ -402,10 +413,6 @@ export default class MessagePanel extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
_getEventTiles() {
|
_getEventTiles() {
|
||||||
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
|
||||||
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
|
||||||
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
|
||||||
|
|
||||||
this.eventNodes = {};
|
this.eventNodes = {};
|
||||||
|
|
||||||
let i;
|
let i;
|
||||||
|
@ -447,199 +454,48 @@ export default class MessagePanel extends React.Component {
|
||||||
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
|
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let grouper = null;
|
||||||
|
|
||||||
for (i = 0; i < this.props.events.length; i++) {
|
for (i = 0; i < this.props.events.length; i++) {
|
||||||
const mxEv = this.props.events[i];
|
const mxEv = this.props.events[i];
|
||||||
const eventId = mxEv.getId();
|
const eventId = mxEv.getId();
|
||||||
const last = (mxEv === lastShownEvent);
|
const last = (mxEv === lastShownEvent);
|
||||||
|
|
||||||
// Wrap initial room creation events into an EventListSummary
|
if (grouper) {
|
||||||
// Grouping only events sent by the same user that sent the `m.room.create` and only until
|
if (grouper.shouldGroup(mxEv)) {
|
||||||
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
|
grouper.add(mxEv);
|
||||||
const shouldGroup = (ev) => {
|
continue;
|
||||||
if (ev.getType() === "m.room.member"
|
} else {
|
||||||
&& (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) {
|
// not part of group, so get the group tiles, close the
|
||||||
return false;
|
// group, and continue like a normal event
|
||||||
}
|
ret.push(...grouper.getTiles());
|
||||||
if (ev.isState() && ev.getSender() === mxEv.getSender()) {
|
prevEvent = grouper.getNewPrevEvent();
|
||||||
return true;
|
grouper = null;
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
// events that we include in the group but then eject out and place
|
|
||||||
// above the group.
|
|
||||||
const shouldEject = (ev) => {
|
|
||||||
if (ev.getType() === "m.room.encryption") return true;
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if (mxEv.getType() === "m.room.create") {
|
|
||||||
let summaryReadMarker = null;
|
|
||||||
const ts1 = mxEv.getTs();
|
|
||||||
|
|
||||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
|
||||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
|
|
||||||
ret.push(dateSeparator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If RM event is the first in the summary, append the RM after the summary
|
|
||||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
|
|
||||||
|
|
||||||
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
|
||||||
if (this._shouldShowEvent(mxEv)) {
|
|
||||||
// pass in the mxEv as prevEvent as well so no extra DateSeparator is rendered
|
|
||||||
ret.push(...this._getTilesForEvent(mxEv, mxEv, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
const summarisedEvents = []; // Don't add m.room.create here as we don't want it inside the summary
|
|
||||||
const ejectedEvents = [];
|
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
|
||||||
const collapsedMxEv = this.props.events[i + 1];
|
|
||||||
|
|
||||||
// Ignore redacted/hidden member events
|
|
||||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
|
||||||
// If this hidden event is the RM and in or at end of a summary put RM after the summary.
|
|
||||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!shouldGroup(collapsedMxEv) || this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If RM event is in the summary, mark it as such and the RM will be appended after the summary.
|
|
||||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
|
||||||
|
|
||||||
if (shouldEject(collapsedMxEv)) {
|
|
||||||
ejectedEvents.push(collapsedMxEv);
|
|
||||||
} else {
|
|
||||||
summarisedEvents.push(collapsedMxEv);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
|
||||||
const eventTiles = summarisedEvents.map((e) => {
|
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
|
||||||
// of EventListSummary, render each member event as if the previous
|
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
|
||||||
// timestamp of the current event, and no DateSeparator is inserted.
|
|
||||||
return this._getTilesForEvent(e, e, e === lastShownEvent);
|
|
||||||
}).reduce((a, b) => a.concat(b), []);
|
|
||||||
|
|
||||||
for (const ejected of ejectedEvents) {
|
|
||||||
ret.push(...this._getTilesForEvent(mxEv, ejected, last));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
|
||||||
const ev = this.props.events[i];
|
|
||||||
ret.push(<EventListSummary
|
|
||||||
key="roomcreationsummary"
|
|
||||||
events={summarisedEvents}
|
|
||||||
onToggle={this._onHeightChanged} // Update scroll state
|
|
||||||
summaryMembers={[ev.sender]}
|
|
||||||
summaryText={_t("%(creator)s created and configured the room.", {
|
|
||||||
creator: ev.sender ? ev.sender.name : ev.getSender(),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
{ eventTiles }
|
|
||||||
</EventListSummary>);
|
|
||||||
|
|
||||||
if (summaryReadMarker) {
|
|
||||||
ret.push(summaryReadMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEvent = mxEv;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const wantTile = this._shouldShowEvent(mxEv);
|
for (const Grouper of groupers) {
|
||||||
|
if (Grouper.canStartGroup(this, mxEv)) {
|
||||||
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent);
|
||||||
if (isMembershipChange(mxEv) && wantTile) {
|
|
||||||
let summaryReadMarker = null;
|
|
||||||
const ts1 = mxEv.getTs();
|
|
||||||
// Ensure that the key of the MemberEventListSummary does not change with new
|
|
||||||
// member events. This will prevent it from being re-created unnecessarily, and
|
|
||||||
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
|
||||||
// method on MELS can be used to prevent unnecessary renderings.
|
|
||||||
//
|
|
||||||
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
|
|
||||||
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
|
|
||||||
// membership event, which will not change during forward pagination.
|
|
||||||
const key = "membereventlistsummary-" + (prevEvent ? mxEv.getId() : "initial");
|
|
||||||
|
|
||||||
if (this._wantsDateSeparator(prevEvent, mxEv.getDate())) {
|
|
||||||
const dateSeparator = <li key={ts1+'~'}><DateSeparator key={ts1+'~'} ts={ts1} /></li>;
|
|
||||||
ret.push(dateSeparator);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If RM event is the first in the MELS, append the RM after MELS
|
|
||||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(mxEv.getId());
|
|
||||||
|
|
||||||
const summarisedEvents = [mxEv];
|
|
||||||
for (;i + 1 < this.props.events.length; i++) {
|
|
||||||
const collapsedMxEv = this.props.events[i + 1];
|
|
||||||
|
|
||||||
// Ignore redacted/hidden member events
|
|
||||||
if (!this._shouldShowEvent(collapsedMxEv)) {
|
|
||||||
// If this hidden event is the RM and in or at end of a MELS put RM after MELS.
|
|
||||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isMembershipChange(collapsedMxEv) ||
|
|
||||||
this._wantsDateSeparator(mxEv, collapsedMxEv.getDate())) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If RM event is in MELS mark it as such and the RM will be appended after MELS.
|
|
||||||
summaryReadMarker = summaryReadMarker || this._readMarkerForEvent(collapsedMxEv.getId());
|
|
||||||
|
|
||||||
summarisedEvents.push(collapsedMxEv);
|
|
||||||
}
|
|
||||||
|
|
||||||
let highlightInMels = false;
|
|
||||||
|
|
||||||
// At this point, i = the index of the last event in the summary sequence
|
|
||||||
let eventTiles = summarisedEvents.map((e) => {
|
|
||||||
if (e.getId() === this.props.highlightedEventId) {
|
|
||||||
highlightInMels = true;
|
|
||||||
}
|
|
||||||
// In order to prevent DateSeparators from appearing in the expanded form
|
|
||||||
// of MemberEventListSummary, render each member event as if the previous
|
|
||||||
// one was itself. This way, the timestamp of the previous event === the
|
|
||||||
// timestamp of the current event, and no DateSeparator is inserted.
|
|
||||||
return this._getTilesForEvent(e, e, e === lastShownEvent);
|
|
||||||
}).reduce((a, b) => a.concat(b), []);
|
|
||||||
|
|
||||||
if (eventTiles.length === 0) {
|
|
||||||
eventTiles = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret.push(<MemberEventListSummary key={key}
|
|
||||||
events={summarisedEvents}
|
|
||||||
onToggle={this._onHeightChanged} // Update scroll state
|
|
||||||
startExpanded={highlightInMels}
|
|
||||||
>
|
|
||||||
{ eventTiles }
|
|
||||||
</MemberEventListSummary>);
|
|
||||||
|
|
||||||
if (summaryReadMarker) {
|
|
||||||
ret.push(summaryReadMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
prevEvent = mxEv;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
if (!grouper) {
|
||||||
|
const wantTile = this._shouldShowEvent(mxEv);
|
||||||
|
if (wantTile) {
|
||||||
|
// make sure we unpack the array returned by _getTilesForEvent,
|
||||||
|
// otherwise react will auto-generate keys and we will end up
|
||||||
|
// replacing all of the DOM elements every time we paginate.
|
||||||
|
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
|
||||||
|
prevEvent = mxEv;
|
||||||
|
}
|
||||||
|
|
||||||
if (wantTile) {
|
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
|
||||||
// make sure we unpack the array returned by _getTilesForEvent,
|
if (readMarker) ret.push(readMarker);
|
||||||
// otherwise react will auto-generate keys and we will end up
|
|
||||||
// replacing all of the DOM elements every time we paginate.
|
|
||||||
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last));
|
|
||||||
prevEvent = mxEv;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
|
if (grouper) {
|
||||||
if (readMarker) ret.push(readMarker);
|
ret.push(...grouper.getTiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
|
@ -921,7 +777,7 @@ export default class MessagePanel extends React.Component {
|
||||||
);
|
);
|
||||||
|
|
||||||
let whoIsTyping;
|
let whoIsTyping;
|
||||||
if (this.props.room && !this.props.tileShape) {
|
if (this.props.room && !this.props.tileShape && this.state.showTypingNotifications) {
|
||||||
whoIsTyping = (<WhoIsTypingTile
|
whoIsTyping = (<WhoIsTypingTile
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
onShown={this._onTypingShown}
|
onShown={this._onTypingShown}
|
||||||
|
@ -950,3 +806,222 @@ export default class MessagePanel extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Grouper classes determine when events can be grouped together in a summary.
|
||||||
|
* Groupers should have the following methods:
|
||||||
|
* - canStartGroup (static): determines if a new group should be started with the
|
||||||
|
* given event
|
||||||
|
* - shouldGroup: determines if the given event should be added to an existing group
|
||||||
|
* - add: adds an event to an existing group (should only be called if shouldGroup
|
||||||
|
* return true)
|
||||||
|
* - getTiles: returns the tiles that represent the group
|
||||||
|
* - getNewPrevEvent: returns the event that should be used as the new prevEvent
|
||||||
|
* when determining things such as whether a date separator is necessary
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Wrap initial room creation events into an EventListSummary
|
||||||
|
// Grouping only events sent by the same user that sent the `m.room.create` and only until
|
||||||
|
// the first non-state event or membership event which is not regarding the sender of the `m.room.create` event
|
||||||
|
class CreationGrouper {
|
||||||
|
static canStartGroup = function(panel, ev) {
|
||||||
|
return ev.getType() === "m.room.create";
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(panel, createEvent, prevEvent, lastShownEvent) {
|
||||||
|
this.panel = panel;
|
||||||
|
this.createEvent = createEvent;
|
||||||
|
this.prevEvent = prevEvent;
|
||||||
|
this.lastShownEvent = lastShownEvent;
|
||||||
|
this.events = [];
|
||||||
|
// events that we include in the group but then eject out and place
|
||||||
|
// above the group.
|
||||||
|
this.ejectedEvents = [];
|
||||||
|
this.readMarker = panel._readMarkerForEvent(createEvent.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldGroup(ev) {
|
||||||
|
const panel = this.panel;
|
||||||
|
const createEvent = this.createEvent;
|
||||||
|
if (!panel._shouldShowEvent(ev)) {
|
||||||
|
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (panel._wantsDateSeparator(this.createEvent, ev.getDate())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ev.getType() === "m.room.member"
|
||||||
|
&& (ev.getStateKey() !== createEvent.getSender() || ev.getContent()["membership"] !== "join")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (ev.isState() && ev.getSender() === createEvent.getSender()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
add(ev) {
|
||||||
|
const panel = this.panel;
|
||||||
|
this.readMarker = this.readMarker || panel._readMarkerForEvent(ev.getId());
|
||||||
|
if (!panel._shouldShowEvent(ev)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ev.getType() === "m.room.encryption") {
|
||||||
|
this.ejectedEvents.push(ev);
|
||||||
|
} else {
|
||||||
|
this.events.push(ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getTiles() {
|
||||||
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
|
||||||
|
|
||||||
|
const panel = this.panel;
|
||||||
|
const ret = [];
|
||||||
|
const createEvent = this.createEvent;
|
||||||
|
const lastShownEvent = this.lastShownEvent;
|
||||||
|
|
||||||
|
if (panel._wantsDateSeparator(this.prevEvent, createEvent.getDate())) {
|
||||||
|
const ts = createEvent.getTs();
|
||||||
|
ret.push(
|
||||||
|
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this m.room.create event should be shown (room upgrade) then show it before the summary
|
||||||
|
if (panel._shouldShowEvent(createEvent)) {
|
||||||
|
// pass in the createEvent as prevEvent as well so no extra DateSeparator is rendered
|
||||||
|
ret.push(...panel._getTilesForEvent(createEvent, createEvent, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ejected of this.ejectedEvents) {
|
||||||
|
ret.push(...panel._getTilesForEvent(
|
||||||
|
createEvent, ejected, createEvent === lastShownEvent,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventTiles = this.events.map((e) => {
|
||||||
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
|
// of EventListSummary, render each member event as if the previous
|
||||||
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
|
// timestamp of the current event, and no DateSeparator is inserted.
|
||||||
|
return panel._getTilesForEvent(e, e, e === lastShownEvent);
|
||||||
|
}).reduce((a, b) => a.concat(b), []);
|
||||||
|
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||||
|
const ev = this.events[this.events.length - 1];
|
||||||
|
ret.push(
|
||||||
|
<EventListSummary
|
||||||
|
key="roomcreationsummary"
|
||||||
|
events={this.events}
|
||||||
|
onToggle={panel._onHeightChanged} // Update scroll state
|
||||||
|
summaryMembers={[ev.sender]}
|
||||||
|
summaryText={_t("%(creator)s created and configured the room.", {
|
||||||
|
creator: ev.sender ? ev.sender.name : ev.getSender(),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{ eventTiles }
|
||||||
|
</EventListSummary>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.readMarker) {
|
||||||
|
ret.push(this.readMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewPrevEvent() {
|
||||||
|
return this.createEvent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap consecutive member events in a ListSummary, ignore if redacted
|
||||||
|
class MemberGrouper {
|
||||||
|
static canStartGroup = function(panel, ev) {
|
||||||
|
return panel._shouldShowEvent(ev) && isMembershipChange(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(panel, ev, prevEvent, lastShownEvent) {
|
||||||
|
this.panel = panel;
|
||||||
|
this.readMarker = panel._readMarkerForEvent(ev.getId());
|
||||||
|
this.events = [ev];
|
||||||
|
this.prevEvent = prevEvent;
|
||||||
|
this.lastShownEvent = lastShownEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldGroup(ev) {
|
||||||
|
return isMembershipChange(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
add(ev) {
|
||||||
|
this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId());
|
||||||
|
this.events.push(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTiles() {
|
||||||
|
const DateSeparator = sdk.getComponent('messages.DateSeparator');
|
||||||
|
const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
|
||||||
|
|
||||||
|
const panel = this.panel;
|
||||||
|
const lastShownEvent = this.lastShownEvent;
|
||||||
|
const ret = [];
|
||||||
|
|
||||||
|
if (panel._wantsDateSeparator(this.prevEvent, this.events[0].getDate())) {
|
||||||
|
const ts = this.events[0].getTs();
|
||||||
|
ret.push(
|
||||||
|
<li key={ts+'~'}><DateSeparator key={ts+'~'} ts={ts} /></li>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the key of the MemberEventListSummary does not change with new
|
||||||
|
// member events. This will prevent it from being re-created unnecessarily, and
|
||||||
|
// instead will allow new props to be provided. In turn, the shouldComponentUpdate
|
||||||
|
// method on MELS can be used to prevent unnecessary renderings.
|
||||||
|
//
|
||||||
|
// Whilst back-paginating with a MELS at the top of the panel, prevEvent will be null,
|
||||||
|
// so use the key "membereventlistsummary-initial". Otherwise, use the ID of the first
|
||||||
|
// membership event, which will not change during forward pagination.
|
||||||
|
const key = "membereventlistsummary-" + (
|
||||||
|
this.prevEvent ? this.events[0].getId() : "initial"
|
||||||
|
);
|
||||||
|
|
||||||
|
let highlightInMels;
|
||||||
|
let eventTiles = this.events.map((e) => {
|
||||||
|
if (e.getId() === panel.props.highlightedEventId) {
|
||||||
|
highlightInMels = true;
|
||||||
|
}
|
||||||
|
// In order to prevent DateSeparators from appearing in the expanded form
|
||||||
|
// of MemberEventListSummary, render each member event as if the previous
|
||||||
|
// one was itself. This way, the timestamp of the previous event === the
|
||||||
|
// timestamp of the current event, and no DateSeparator is inserted.
|
||||||
|
return panel._getTilesForEvent(e, e, e === lastShownEvent);
|
||||||
|
}).reduce((a, b) => a.concat(b), []);
|
||||||
|
|
||||||
|
if (eventTiles.length === 0) {
|
||||||
|
eventTiles = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.push(
|
||||||
|
<MemberEventListSummary key={key}
|
||||||
|
events={this.events}
|
||||||
|
onToggle={panel._onHeightChanged} // Update scroll state
|
||||||
|
startExpanded={highlightInMels}
|
||||||
|
>
|
||||||
|
{ eventTiles }
|
||||||
|
</MemberEventListSummary>,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (this.readMarker) {
|
||||||
|
ret.push(this.readMarker);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
getNewPrevEvent() {
|
||||||
|
return this.events[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// all the grouper classes that we use
|
||||||
|
const groupers = [CreationGrouper, MemberGrouper];
|
||||||
|
|
|
@ -92,6 +92,7 @@ export default class RightPanel extends React.Component {
|
||||||
// not mounted in time to get the dispatch.
|
// not mounted in time to get the dispatch.
|
||||||
// Until then, let this code serve as a warning from history.
|
// Until then, let this code serve as a warning from history.
|
||||||
if (
|
if (
|
||||||
|
rps.roomPanelPhaseParams.member &&
|
||||||
userForPanel.userId === rps.roomPanelPhaseParams.member.userId &&
|
userForPanel.userId === rps.roomPanelPhaseParams.member.userId &&
|
||||||
rps.roomPanelPhaseParams.verificationRequest
|
rps.roomPanelPhaseParams.verificationRequest
|
||||||
) {
|
) {
|
||||||
|
@ -285,7 +286,7 @@ export default class RightPanel extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className={classes}>
|
<aside className={classes} id="mx_RightPanel">
|
||||||
{ panel }
|
{ panel }
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
|
|
|
@ -460,8 +460,6 @@ export default createReactClass({
|
||||||
// (We could use isMounted, but facebook have deprecated that.)
|
// (We could use isMounted, but facebook have deprecated that.)
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
|
|
||||||
SettingsStore.unwatchSetting(this._ciderWatcherRef);
|
|
||||||
|
|
||||||
// update the scroll map before we get unmounted
|
// update the scroll map before we get unmounted
|
||||||
if (this.state.roomId) {
|
if (this.state.roomId) {
|
||||||
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
RoomScrollStateStore.setScrollState(this.state.roomId, this._getScrollState());
|
||||||
|
@ -811,7 +809,9 @@ export default createReactClass({
|
||||||
debuglog("e2e verified", verified, "unverified", unverified);
|
debuglog("e2e verified", verified, "unverified", unverified);
|
||||||
|
|
||||||
/* Check all verified user devices. */
|
/* Check all verified user devices. */
|
||||||
for (const userId of [...verified, cli.getUserId()]) {
|
/* Don't alarm if no other users are verified */
|
||||||
|
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
|
||||||
|
for (const userId of targets) {
|
||||||
const devices = await cli.getStoredDevicesForUser(userId);
|
const devices = await cli.getStoredDevicesForUser(userId);
|
||||||
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
const anyDeviceNotVerified = devices.some(({deviceId}) => {
|
||||||
return !cli.checkDeviceTrust(userId, deviceId).isVerified();
|
return !cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||||
|
|
|
@ -160,6 +160,7 @@ export default createReactClass({
|
||||||
onKeyDown={ this._onKeyDown }
|
onKeyDown={ this._onKeyDown }
|
||||||
onBlur={this._onBlur}
|
onBlur={this._onBlur}
|
||||||
placeholder={ placeholder }
|
placeholder={ placeholder }
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
{ clearButton }
|
{ clearButton }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -83,12 +83,13 @@ export default class CompleteSecurity extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVerificationRequest = (request) => {
|
onVerificationRequest = async (request) => {
|
||||||
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
|
if (request.otherUserId !== MatrixClientPeg.get().getUserId()) return;
|
||||||
|
|
||||||
if (this.state.verificationRequest) {
|
if (this.state.verificationRequest) {
|
||||||
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
this.state.verificationRequest.off("change", this.onVerificationRequestChange);
|
||||||
}
|
}
|
||||||
|
await request.accept();
|
||||||
request.on("change", this.onVerificationRequestChange);
|
request.on("change", this.onVerificationRequestChange);
|
||||||
this.setState({
|
this.setState({
|
||||||
verificationRequest: request,
|
verificationRequest: request,
|
||||||
|
@ -138,9 +139,12 @@ export default class CompleteSecurity extends React.Component {
|
||||||
let body;
|
let body;
|
||||||
|
|
||||||
if (this.state.verificationRequest) {
|
if (this.state.verificationRequest) {
|
||||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||||
body = <IncomingSasDialog verifier={this.state.verificationRequest.verifier}
|
body = <EncryptionPanel
|
||||||
onFinished={this.props.onFinished}
|
layout="dialog"
|
||||||
|
verificationRequest={this.state.verificationRequest}
|
||||||
|
onClose={this.props.onFinished}
|
||||||
|
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
|
||||||
/>;
|
/>;
|
||||||
} else if (phase === PHASE_INTRO) {
|
} else if (phase === PHASE_INTRO) {
|
||||||
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning"></span>;
|
||||||
|
|
|
@ -481,7 +481,7 @@ export default createReactClass({
|
||||||
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
"Either use HTTPS or <a>enable unsafe scripts</a>.", {},
|
||||||
{
|
{
|
||||||
'a': (sub) => {
|
'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"
|
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||||
>
|
>
|
||||||
{ sub }
|
{ sub }
|
||||||
|
@ -496,11 +496,10 @@ export default createReactClass({
|
||||||
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
|
||||||
"is not blocking requests.", {},
|
"is not blocking requests.", {},
|
||||||
{
|
{
|
||||||
'a': (sub) => {
|
'a': (sub) =>
|
||||||
return <a target="_blank" rel="noopener" href={this.props.serverConfig.hsUrl}>
|
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||||
{ sub }
|
{ sub }
|
||||||
</a>;
|
</a>,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
) }
|
) }
|
||||||
</span>;
|
</span>;
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default createReactClass({
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
<div className="mx_AuthFooter">
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -61,13 +61,9 @@ export default createReactClass({
|
||||||
} else {
|
} else {
|
||||||
console.log("Loading recaptcha script...");
|
console.log("Loading recaptcha script...");
|
||||||
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
window.mx_on_recaptcha_loaded = () => {this._onCaptchaLoaded();};
|
||||||
let protocol = global.location.protocol;
|
|
||||||
if (protocol !== "http:") {
|
|
||||||
protocol = "https:";
|
|
||||||
}
|
|
||||||
const scriptTag = document.createElement('script');
|
const scriptTag = document.createElement('script');
|
||||||
scriptTag.setAttribute(
|
scriptTag.setAttribute(
|
||||||
'src', `${protocol}//www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
'src', `https://www.recaptcha.net/recaptcha/api.js?onload=mx_on_recaptcha_loaded&render=explicit`,
|
||||||
);
|
);
|
||||||
this._recaptchaContainer.current.appendChild(scriptTag);
|
this._recaptchaContainer.current.appendChild(scriptTag);
|
||||||
}
|
}
|
||||||
|
|
|
@ -331,7 +331,7 @@ export const TermsAuthEntry = createReactClass({
|
||||||
checkboxes.push(
|
checkboxes.push(
|
||||||
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
<label key={"policy_checkbox_" + policy.id} className="mx_InteractiveAuthEntryComponents_termsPolicy">
|
||||||
<input type="checkbox" onChange={() => this._togglePolicy(policy.id)} checked={checked} />
|
<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>,
|
</label>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -604,6 +604,7 @@ export const FallbackAuthEntry = createReactClass({
|
||||||
this.props.authSessionId,
|
this.props.authSessionId,
|
||||||
);
|
);
|
||||||
this._popupWindow = window.open(url);
|
this._popupWindow = window.open(url);
|
||||||
|
this._popupWindow.opener = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
_onReceiveMessage: function(event) {
|
_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 " +
|
"Enter the location of your Modular homeserver. It may use your own " +
|
||||||
"domain name or be a subdomain of <a>modular.im</a>.",
|
"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}
|
{sub}
|
||||||
</a>,
|
</a>,
|
||||||
},
|
},
|
||||||
|
|
|
@ -486,6 +486,7 @@ export default createReactClass({
|
||||||
id="mx_RegistrationForm_password"
|
id="mx_RegistrationForm_password"
|
||||||
ref={field => this[FIELD_PASSWORD] = field}
|
ref={field => this[FIELD_PASSWORD] = field}
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
label={_t("Password")}
|
label={_t("Password")}
|
||||||
value={this.state.password}
|
value={this.state.password}
|
||||||
onChange={this.onPasswordChange}
|
onChange={this.onPasswordChange}
|
||||||
|
@ -499,6 +500,7 @@ export default createReactClass({
|
||||||
id="mx_RegistrationForm_passwordConfirm"
|
id="mx_RegistrationForm_passwordConfirm"
|
||||||
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
ref={field => this[FIELD_PASSWORD_CONFIRM] = field}
|
||||||
type="password"
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
label={_t("Confirm")}
|
label={_t("Confirm")}
|
||||||
value={this.state.passwordConfirm}
|
value={this.state.passwordConfirm}
|
||||||
onChange={this.onPasswordConfirmChange}
|
onChange={this.onPasswordConfirmChange}
|
||||||
|
|
|
@ -274,15 +274,13 @@ export default class ServerConfig extends React.PureComponent {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_ServerConfig">
|
<form className="mx_ServerConfig" onSubmit={this.onSubmit} autoComplete="off">
|
||||||
<h3>{_t("Other servers")}</h3>
|
<h3>{_t("Other servers")}</h3>
|
||||||
{errorText}
|
{errorText}
|
||||||
{this._renderHomeserverSection()}
|
{this._renderHomeserverSection()}
|
||||||
{this._renderIdentityServerSection()}
|
{this._renderIdentityServerSection()}
|
||||||
<form onSubmit={this.onSubmit} autoComplete="off" action={null}>
|
{submitButton}
|
||||||
{submitButton}
|
</form>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const TYPES = {
|
||||||
label: () => _t('Premium'),
|
label: () => _t('Premium'),
|
||||||
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
logo: () => <img src={require('../../../../res/img/modular-bw-logo.svg')} />,
|
||||||
description: () => _t('Premium hosting for organisations <a>Learn more</a>', {}, {
|
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}
|
{sub}
|
||||||
</a>,
|
</a>,
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -414,11 +414,16 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
// XXX: if we use room ID, we should also include a server where the event can be found (other than in the domain of the event ID)
|
||||||
const permalinkButton = (
|
const permalinkButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field">
|
<MenuItem
|
||||||
<a href={permalink} target="_blank" rel="noopener" onClick={this.onPermalinkClick} tabIndex={-1}>
|
element="a"
|
||||||
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
className="mx_MessageContextMenu_field"
|
||||||
? _t('Share Permalink') : _t('Share Message') }
|
onClick={this.onPermalinkClick}
|
||||||
</a>
|
href={permalink}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
{ mxEvent.isRedacted() || mxEvent.getType() !== 'm.room.message'
|
||||||
|
? _t('Share Permalink') : _t('Share Message') }
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -436,16 +441,15 @@ export default createReactClass({
|
||||||
isUrlPermitted(mxEvent.event.content.external_url)
|
isUrlPermitted(mxEvent.event.content.external_url)
|
||||||
) {
|
) {
|
||||||
externalURLButton = (
|
externalURLButton = (
|
||||||
<MenuItem className="mx_MessageContextMenu_field">
|
<MenuItem
|
||||||
<a
|
element="a"
|
||||||
href={mxEvent.event.content.external_url}
|
className="mx_MessageContextMenu_field"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noreferrer noopener"
|
||||||
onClick={this.closeMenu}
|
onClick={this.closeMenu}
|
||||||
tabIndex={-1}
|
href={mxEvent.event.content.external_url}
|
||||||
>
|
>
|
||||||
{ _t('Source URL') }
|
{ _t('Source URL') }
|
||||||
</a>
|
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,10 +68,11 @@ export default class TopLeftMenu extends React.Component {
|
||||||
{_t(
|
{_t(
|
||||||
"<a>Upgrade</a> to your own domain", {},
|
"<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='' />
|
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||||
</a>
|
</a>
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
@ -52,7 +52,7 @@ export default class ChangelogDialog extends React.Component {
|
||||||
_elementsForCommit(commit) {
|
_elementsForCommit(commit) {
|
||||||
return (
|
return (
|
||||||
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
<li key={commit.sha} className="mx_ChangelogDialog_li">
|
||||||
<a href={commit.html_url} target="_blank" rel="noopener">
|
<a href={commit.html_url} target="_blank" rel="noreferrer noopener">
|
||||||
{commit.commit.message.split('\n')[0]}
|
{commit.commit.message.split('\n')[0]}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
/*
|
||||||
|
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 {_t} from "../../../languageHandler";
|
||||||
|
import * as sdk from "../../../index";
|
||||||
|
|
||||||
|
export default class ConfirmDestroyCrossSigningDialog extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
_onConfirm = () => {
|
||||||
|
this.props.onFinished(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
_onDecline = () => {
|
||||||
|
this.props.onFinished(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog
|
||||||
|
className='mx_ConfirmDestroyCrossSigningDialog'
|
||||||
|
hasCancel={true}
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={_t("Destroy cross-signing keys?")}>
|
||||||
|
<div className='mx_ConfirmDestroyCrossSigningDialog_content'>
|
||||||
|
<p>
|
||||||
|
{_t(
|
||||||
|
"Deleting cross-signing keys is permanent. " +
|
||||||
|
"Anyone you have verified with will see security alerts. " +
|
||||||
|
"You almost certainly don't want to do this, unless " +
|
||||||
|
"you've lost every device you can cross-sign from.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Clear cross-signing keys")}
|
||||||
|
onPrimaryButtonClick={this._onConfirm}
|
||||||
|
primaryButtonClass="danger"
|
||||||
|
cancelButton={_t("Cancel")}
|
||||||
|
onCancel={this._onDecline}
|
||||||
|
/>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -118,6 +118,7 @@ export default class DeactivateAccountDialog extends React.Component {
|
||||||
|
|
||||||
const Field = sdk.getComponent('elements.Field');
|
const Field = sdk.getComponent('elements.Field');
|
||||||
|
|
||||||
|
// this is on purpose not a <form /> to prevent Enter triggering submission, to further prevent accidents
|
||||||
return (
|
return (
|
||||||
<BaseDialog className="mx_DeactivateAccountDialog"
|
<BaseDialog className="mx_DeactivateAccountDialog"
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
|
|
|
@ -27,16 +27,19 @@ import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||||
import {ensureDMExists} from "../../../createRoom";
|
import {ensureDMExists} from "../../../createRoom";
|
||||||
import dis from "../../../dispatcher";
|
import dis from "../../../dispatcher";
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
|
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
|
import VerificationQREmojiOptions from "../verification/VerificationQREmojiOptions";
|
||||||
|
|
||||||
const MODE_LEGACY = 'legacy';
|
const MODE_LEGACY = 'legacy';
|
||||||
const MODE_SAS = 'sas';
|
const MODE_SAS = 'sas';
|
||||||
|
|
||||||
const PHASE_START = 0;
|
const PHASE_START = 0;
|
||||||
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
|
const PHASE_WAIT_FOR_PARTNER_TO_ACCEPT = 1;
|
||||||
const PHASE_SHOW_SAS = 2;
|
const PHASE_PICK_VERIFICATION_OPTION = 2;
|
||||||
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 3;
|
const PHASE_SHOW_SAS = 3;
|
||||||
const PHASE_VERIFIED = 4;
|
const PHASE_WAIT_FOR_PARTNER_TO_CONFIRM = 4;
|
||||||
const PHASE_CANCELLED = 5;
|
const PHASE_VERIFIED = 5;
|
||||||
|
const PHASE_CANCELLED = 6;
|
||||||
|
|
||||||
export default class DeviceVerifyDialog extends React.Component {
|
export default class DeviceVerifyDialog extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -49,6 +52,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
super();
|
super();
|
||||||
this._verifier = null;
|
this._verifier = null;
|
||||||
this._showSasEvent = null;
|
this._showSasEvent = null;
|
||||||
|
this._request = null;
|
||||||
this.state = {
|
this.state = {
|
||||||
phase: PHASE_START,
|
phase: PHASE_START,
|
||||||
mode: MODE_SAS,
|
mode: MODE_SAS,
|
||||||
|
@ -80,6 +84,25 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onUseSasClick = async () => {
|
||||||
|
try {
|
||||||
|
this._verifier = this._request.beginKeyVerification(verificationMethods.SAS);
|
||||||
|
this._verifier.on('show_sas', this._onVerifierShowSas);
|
||||||
|
// throws upon cancellation
|
||||||
|
await this._verifier.verify();
|
||||||
|
this.setState({phase: PHASE_VERIFIED});
|
||||||
|
this._verifier.removeListener('show_sas', this._onVerifierShowSas);
|
||||||
|
this._verifier = null;
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Verification failed", e);
|
||||||
|
this.setState({
|
||||||
|
phase: PHASE_CANCELLED,
|
||||||
|
});
|
||||||
|
this._verifier = null;
|
||||||
|
this._request = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
_onLegacyFinished = (confirm) => {
|
_onLegacyFinished = (confirm) => {
|
||||||
if (confirm) {
|
if (confirm) {
|
||||||
MatrixClientPeg.get().setDeviceVerified(
|
MatrixClientPeg.get().setDeviceVerified(
|
||||||
|
@ -100,7 +123,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
const roomId = await ensureDMExistsAndOpen(this.props.userId);
|
||||||
// throws upon cancellation before having started
|
// throws upon cancellation before having started
|
||||||
const request = await client.requestVerificationDM(
|
const request = await client.requestVerificationDM(
|
||||||
this.props.userId, roomId, [verificationMethods.SAS],
|
this.props.userId, roomId,
|
||||||
);
|
);
|
||||||
await request.waitFor(r => r.ready || r.started);
|
await request.waitFor(r => r.ready || r.started);
|
||||||
if (request.ready) {
|
if (request.ready) {
|
||||||
|
@ -108,11 +131,21 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
} else {
|
} else {
|
||||||
this._verifier = request.verifier;
|
this._verifier = request.verifier;
|
||||||
}
|
}
|
||||||
|
} else if (verifyingOwnDevice && SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
this._request = await client.requestVerification(this.props.userId, [
|
||||||
|
verificationMethods.SAS,
|
||||||
|
SHOW_QR_CODE_METHOD,
|
||||||
|
verificationMethods.RECIPROCATE_QR_CODE,
|
||||||
|
]);
|
||||||
|
|
||||||
|
await this._request.waitFor(r => r.ready || r.started);
|
||||||
|
this.setState({phase: PHASE_PICK_VERIFICATION_OPTION});
|
||||||
} else {
|
} else {
|
||||||
this._verifier = client.beginKeyVerification(
|
this._verifier = client.beginKeyVerification(
|
||||||
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
|
verificationMethods.SAS, this.props.userId, this.props.device.deviceId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!this._verifier) return;
|
||||||
this._verifier.on('show_sas', this._onVerifierShowSas);
|
this._verifier.on('show_sas', this._onVerifierShowSas);
|
||||||
// throws upon cancellation
|
// throws upon cancellation
|
||||||
await this._verifier.verify();
|
await this._verifier.verify();
|
||||||
|
@ -150,10 +183,13 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
let body;
|
let body;
|
||||||
switch (this.state.phase) {
|
switch (this.state.phase) {
|
||||||
case PHASE_START:
|
case PHASE_START:
|
||||||
body = this._renderSasVerificationPhaseStart();
|
body = this._renderVerificationPhaseStart();
|
||||||
break;
|
break;
|
||||||
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
|
case PHASE_WAIT_FOR_PARTNER_TO_ACCEPT:
|
||||||
body = this._renderSasVerificationPhaseWaitAccept();
|
body = this._renderVerificationPhaseWaitAccept();
|
||||||
|
break;
|
||||||
|
case PHASE_PICK_VERIFICATION_OPTION:
|
||||||
|
body = this._renderVerificationPhasePick();
|
||||||
break;
|
break;
|
||||||
case PHASE_SHOW_SAS:
|
case PHASE_SHOW_SAS:
|
||||||
body = this._renderSasVerificationPhaseShowSas();
|
body = this._renderSasVerificationPhaseShowSas();
|
||||||
|
@ -162,10 +198,10 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
|
body = this._renderSasVerificationPhaseWaitForPartnerToConfirm();
|
||||||
break;
|
break;
|
||||||
case PHASE_VERIFIED:
|
case PHASE_VERIFIED:
|
||||||
body = this._renderSasVerificationPhaseVerified();
|
body = this._renderVerificationPhaseVerified();
|
||||||
break;
|
break;
|
||||||
case PHASE_CANCELLED:
|
case PHASE_CANCELLED:
|
||||||
body = this._renderSasVerificationPhaseCancelled();
|
body = this._renderVerificationPhaseCancelled();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,7 +216,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSasVerificationPhaseStart() {
|
_renderVerificationPhaseStart() {
|
||||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return (
|
return (
|
||||||
|
@ -206,7 +242,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSasVerificationPhaseWaitAccept() {
|
_renderVerificationPhaseWaitAccept() {
|
||||||
const Spinner = sdk.getComponent("views.elements.Spinner");
|
const Spinner = sdk.getComponent("views.elements.Spinner");
|
||||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||||
|
|
||||||
|
@ -227,6 +263,14 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_renderVerificationPhasePick() {
|
||||||
|
return <VerificationQREmojiOptions
|
||||||
|
request={this._request}
|
||||||
|
onCancel={this._onCancelClick}
|
||||||
|
onStartEmoji={this._onUseSasClick}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
_renderSasVerificationPhaseShowSas() {
|
_renderSasVerificationPhaseShowSas() {
|
||||||
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
const VerificationShowSas = sdk.getComponent('views.verification.VerificationShowSas');
|
||||||
return <VerificationShowSas
|
return <VerificationShowSas
|
||||||
|
@ -234,6 +278,7 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
onCancel={this._onCancelClick}
|
onCancel={this._onCancelClick}
|
||||||
onDone={this._onSasMatchesClick}
|
onDone={this._onSasMatchesClick}
|
||||||
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
|
isSelf={MatrixClientPeg.get().getUserId() === this.props.userId}
|
||||||
|
onStartEmoji={this._onUseSasClick}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -247,12 +292,12 @@ export default class DeviceVerifyDialog extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSasVerificationPhaseVerified() {
|
_renderVerificationPhaseVerified() {
|
||||||
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
|
const VerificationComplete = sdk.getComponent('views.verification.VerificationComplete');
|
||||||
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
|
return <VerificationComplete onDone={this._onVerifiedDoneClick} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
_renderSasVerificationPhaseCancelled() {
|
_renderVerificationPhaseCancelled() {
|
||||||
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
|
const VerificationCancelled = sdk.getComponent('views.verification.VerificationCancelled');
|
||||||
return <VerificationCancelled onDone={this._onCancelClick} />;
|
return <VerificationCancelled onDone={this._onCancelClick} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React, {useState, useEffect} from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
import SyntaxHighlight from '../elements/SyntaxHighlight';
|
||||||
|
@ -22,6 +22,16 @@ import { _t } from '../../../languageHandler';
|
||||||
import { Room } from "matrix-js-sdk";
|
import { Room } from "matrix-js-sdk";
|
||||||
import Field from "../elements/Field";
|
import Field from "../elements/Field";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
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 {
|
class GenericEditor extends React.PureComponent {
|
||||||
// static propTypes = {onBack: PropTypes.func.isRequired};
|
// 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 = [
|
const Entries = [
|
||||||
SendCustomEvent,
|
SendCustomEvent,
|
||||||
RoomStateExplorer,
|
RoomStateExplorer,
|
||||||
SendAccountData,
|
SendAccountData,
|
||||||
AccountDataExplorer,
|
AccountDataExplorer,
|
||||||
ServersInRoomList,
|
ServersInRoomList,
|
||||||
|
VerificationExplorer,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class DevtoolsDialog extends React.PureComponent {
|
export default class DevtoolsDialog extends React.PureComponent {
|
||||||
|
|
|
@ -31,7 +31,7 @@ import dis from "../../../dispatcher";
|
||||||
import IdentityAuthClient from "../../../IdentityAuthClient";
|
import IdentityAuthClient from "../../../IdentityAuthClient";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import {humanizeTime} from "../../../utils/humanize";
|
import {humanizeTime} from "../../../utils/humanize";
|
||||||
import createRoom from "../../../createRoom";
|
import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
|
||||||
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
import {inviteMultipleToRoom} from "../../../RoomInvite";
|
||||||
import SettingsStore from '../../../settings/SettingsStore';
|
import SettingsStore from '../../../settings/SettingsStore';
|
||||||
|
|
||||||
|
@ -512,9 +512,27 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
return false;
|
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 () => {
|
_startDm = async () => {
|
||||||
this.setState({busy: true});
|
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.
|
// Check if there is already a DM with these people and reuse it if possible.
|
||||||
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
const existingRoom = DMRoomMap.shared().getDMRoomForIdentifiers(targetIds);
|
||||||
|
@ -535,11 +553,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
// Check whether all users have uploaded device keys before.
|
// Check whether all users have uploaded device keys before.
|
||||||
// If so, enable encryption in the new room.
|
// If so, enable encryption in the new room.
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const usersToDevicesMap = await client.downloadKeys(targetIds);
|
const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
|
||||||
const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
|
|
||||||
// `devices` is an object of the form { deviceId: deviceInfo, ... }.
|
|
||||||
return Object.keys(devices).length > 0;
|
|
||||||
});
|
|
||||||
if (allHaveDeviceKeys) {
|
if (allHaveDeviceKeys) {
|
||||||
createRoomOptions.encryption = true;
|
createRoomOptions.encryption = true;
|
||||||
}
|
}
|
||||||
|
@ -548,9 +562,12 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
// Check if it's a traditional DM and create the room if required.
|
// 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
|
// TODO: [Canonical DMs] Remove this check and instead just create the multi-person DM
|
||||||
let createRoomPromise = Promise.resolve();
|
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];
|
createRoomOptions.dmUserId = targetIds[0];
|
||||||
createRoomPromise = createRoom(createRoomOptions);
|
createRoomPromise = createRoom(createRoomOptions);
|
||||||
|
} else if (isSelf) {
|
||||||
|
createRoomPromise = createRoom(createRoomOptions);
|
||||||
} else {
|
} else {
|
||||||
// Create a boring room and try to invite the targets manually.
|
// Create a boring room and try to invite the targets manually.
|
||||||
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
createRoomPromise = createRoom(createRoomOptions).then(roomId => {
|
||||||
|
@ -577,7 +594,9 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
_inviteUsers = () => {
|
_inviteUsers = () => {
|
||||||
this.setState({busy: true});
|
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);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
if (!room) {
|
if (!room) {
|
||||||
|
@ -634,13 +653,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
|
|
||||||
// While we're here, try and autocomplete a search result for the mxid itself
|
// 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 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 {
|
try {
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
const profile = await MatrixClientPeg.get().getProfileInfo(term);
|
||||||
if (profile) {
|
if (profile) {
|
||||||
// If we have a profile, we have enough information to assume that
|
// If we have a profile, we have enough information to assume that
|
||||||
// the mxid can be invited - add it to the list
|
// the mxid can be invited - add it to the list. We stick it at the
|
||||||
r.results.push({
|
// top so it is most obviously presented to the user.
|
||||||
|
r.results.splice(0, 0, {
|
||||||
user_id: term,
|
user_id: term,
|
||||||
display_name: profile['displayname'],
|
display_name: profile['displayname'],
|
||||||
avatar_url: profile['avatar_url'],
|
avatar_url: profile['avatar_url'],
|
||||||
|
@ -649,6 +669,14 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Non-fatal error trying to make an invite for a user ID");
|
console.warn("Non-fatal error trying to make an invite for a user ID");
|
||||||
console.warn(e);
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -773,7 +801,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
];
|
];
|
||||||
const toAdd = [];
|
const toAdd = [];
|
||||||
const failed = [];
|
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) {
|
for (const address of potentialAddresses) {
|
||||||
const member = possibleMembers.find(m => m.userId === address);
|
const member = possibleMembers.find(m => m.userId === address);
|
||||||
if (member) {
|
if (member) {
|
||||||
|
@ -1018,7 +1046,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
"If you can't find someone, ask them for their username, share your " +
|
"If you can't find someone, ask them for their username, share your " +
|
||||||
"username (%(userId)s) or <a>profile link</a>.",
|
"username (%(userId)s) or <a>profile link</a>.",
|
||||||
{userId},
|
{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");
|
buttonText = _t("Go");
|
||||||
goButtonFn = this._startDm;
|
goButtonFn = this._startDm;
|
||||||
|
@ -1027,12 +1055,17 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
helpText = _t(
|
helpText = _t(
|
||||||
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
"If you can't find someone, ask them for their username (e.g. @user:server.com) or " +
|
||||||
"<a>share this room</a>.", {},
|
"<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");
|
buttonText = _t("Invite");
|
||||||
goButtonFn = this._inviteUsers;
|
goButtonFn = this._inviteUsers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSelection = this.state.targets.length > 0
|
||||||
|
|| (this.state.filterText && this.state.filterText.includes('@'));
|
||||||
return (
|
return (
|
||||||
<BaseDialog
|
<BaseDialog
|
||||||
className='mx_InviteDialog'
|
className='mx_InviteDialog'
|
||||||
|
@ -1049,7 +1082,7 @@ export default class InviteDialog extends React.PureComponent {
|
||||||
kind="primary"
|
kind="primary"
|
||||||
onClick={goButtonFn}
|
onClick={goButtonFn}
|
||||||
className='mx_InviteDialog_goButton'
|
className='mx_InviteDialog_goButton'
|
||||||
disabled={this.state.busy}
|
disabled={this.state.busy || !hasSelection}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
|
|
|
@ -19,9 +19,10 @@ import PropTypes from 'prop-types';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
import { replaceableComponent } from '../../../utils/replaceableComponent';
|
||||||
import DeviceVerifyDialog from './DeviceVerifyDialog';
|
import VerificationRequestDialog from './VerificationRequestDialog';
|
||||||
import BaseDialog from './BaseDialog';
|
import BaseDialog from './BaseDialog';
|
||||||
import DialogButtons from '../elements/DialogButtons';
|
import DialogButtons from '../elements/DialogButtons';
|
||||||
|
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
|
|
||||||
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
|
@replaceableComponent("views.dialogs.NewSessionReviewDialog")
|
||||||
export default class NewSessionReviewDialog extends React.PureComponent {
|
export default class NewSessionReviewDialog extends React.PureComponent {
|
||||||
|
@ -35,12 +36,18 @@ export default class NewSessionReviewDialog extends React.PureComponent {
|
||||||
this.props.onFinished(false);
|
this.props.onFinished(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
onContinueClick = () => {
|
onContinueClick = async () => {
|
||||||
const { userId, device } = this.props;
|
const { userId, device } = this.props;
|
||||||
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', DeviceVerifyDialog, {
|
const cli = MatrixClientPeg.get();
|
||||||
|
const request = await cli.requestVerification(
|
||||||
userId,
|
userId,
|
||||||
device,
|
[device.deviceId],
|
||||||
}, null, /* priority = */ false, /* static = */ true);
|
);
|
||||||
|
|
||||||
|
this.props.onFinished(true);
|
||||||
|
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
|
||||||
|
verificationRequest: request,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|
|
@ -31,6 +31,7 @@ export default createReactClass({
|
||||||
danger: PropTypes.bool,
|
danger: PropTypes.bool,
|
||||||
focus: PropTypes.bool,
|
focus: PropTypes.bool,
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
headerImage: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getDefaultProps: function() {
|
getDefaultProps: function() {
|
||||||
|
@ -63,6 +64,7 @@ export default createReactClass({
|
||||||
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
<BaseDialog className="mx_QuestionDialog" onFinished={this.props.onFinished}
|
||||||
title={this.props.title}
|
title={this.props.title}
|
||||||
contentId='mx_Dialog_content'
|
contentId='mx_Dialog_content'
|
||||||
|
headerImage={this.props.headerImage}
|
||||||
hasCancel={this.props.hasCancelButton}
|
hasCancel={this.props.hasCancelButton}
|
||||||
>
|
>
|
||||||
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
<div className="mx_Dialog_content" id='mx_Dialog_content'>
|
||||||
|
|
|
@ -218,7 +218,7 @@ export default class ShareDialog extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div className="mx_ShareDialog_social_container">
|
<div className="mx_ShareDialog_social_container">
|
||||||
{
|
{
|
||||||
socials.map((social) => <a rel="noopener"
|
socials.map((social) => <a rel="noreferrer noopener"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
key={social.name}
|
key={social.name}
|
||||||
name={social.name}
|
name={social.name}
|
||||||
|
|
|
@ -135,7 +135,7 @@ export default class TermsDialog extends React.PureComponent {
|
||||||
rows.push(<tr key={termDoc[termsLang].url}>
|
rows.push(<tr key={termDoc[termsLang].url}>
|
||||||
<td className="mx_TermsDialog_service">{serviceName}</td>
|
<td className="mx_TermsDialog_service">{serviceName}</td>
|
||||||
<td className="mx_TermsDialog_summary">{summary}</td>
|
<td className="mx_TermsDialog_summary">{summary}</td>
|
||||||
<td>{termDoc[termsLang].name} <a rel="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" />
|
<span className="mx_TermsDialog_link" />
|
||||||
</a></td>
|
</a></td>
|
||||||
<td><TermsCheckbox
|
<td><TermsCheckbox
|
||||||
|
|
55
src/components/views/dialogs/VerificationRequestDialog.js
Normal file
55
src/components/views/dialogs/VerificationRequestDialog.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/*
|
||||||
|
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 {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
import * as sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
|
||||||
|
export default class VerificationRequestDialog extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
verificationRequest: PropTypes.object.isRequired,
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.onFinished = this.onFinished.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||||
|
const EncryptionPanel = sdk.getComponent("views.right_panel.EncryptionPanel");
|
||||||
|
return <BaseDialog className="mx_InfoDialog" onFinished={this.onFinished}
|
||||||
|
contentId="mx_Dialog_content"
|
||||||
|
title={_t("Verification Request")}
|
||||||
|
hasCancel={true}
|
||||||
|
>
|
||||||
|
<EncryptionPanel
|
||||||
|
layout="dialog"
|
||||||
|
verificationRequest={this.props.verificationRequest}
|
||||||
|
onClose={this.props.onFinished}
|
||||||
|
member={MatrixClientPeg.get().getUser(this.props.verificationRequest.otherUserId)}
|
||||||
|
/>
|
||||||
|
</BaseDialog>;
|
||||||
|
}
|
||||||
|
|
||||||
|
onFinished() {
|
||||||
|
this.props.verificationRequest.cancel();
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,7 +22,6 @@ import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
import { MatrixClient } from 'matrix-js-sdk';
|
import { MatrixClient } from 'matrix-js-sdk';
|
||||||
import Modal from '../../../../Modal';
|
import Modal from '../../../../Modal';
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import {Key} from "../../../../Keyboard";
|
|
||||||
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
import { accessSecretStorage } from '../../../../CrossSigningManager';
|
||||||
|
|
||||||
const RESTORE_TYPE_PASSPHRASE = 0;
|
const RESTORE_TYPE_PASSPHRASE = 0;
|
||||||
|
@ -125,6 +124,8 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRecoveryKeyNext = async () => {
|
_onRecoveryKeyNext = async () => {
|
||||||
|
if (!this.state.recoveryKeyValid) return;
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
restoreError: null,
|
restoreError: null,
|
||||||
|
@ -157,18 +158,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseKeyPress = (e) => {
|
|
||||||
if (e.key === Key.ENTER) {
|
|
||||||
this._onPassPhraseNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRecoveryKeyKeyPress = (e) => {
|
|
||||||
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
|
|
||||||
this._onRecoveryKeyNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async _restoreWithSecretStorage() {
|
async _restoreWithSecretStorage() {
|
||||||
this.setState({
|
this.setState({
|
||||||
loading: true,
|
loading: true,
|
||||||
|
@ -305,21 +294,22 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
"messaging by entering your recovery passphrase.",
|
"messaging by entering your recovery passphrase.",
|
||||||
)}</p>
|
)}</p>
|
||||||
|
|
||||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
<form className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||||
<input type="password"
|
<input type="password"
|
||||||
className="mx_RestoreKeyBackupDialog_passPhraseInput"
|
className="mx_RestoreKeyBackupDialog_passPhraseInput"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
onKeyPress={this._onPassPhraseKeyPress}
|
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNext}
|
onPrimaryButtonClick={this._onPassPhraseNext}
|
||||||
|
primaryIsSubmit={true}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onCancel={this._onCancel}
|
onCancel={this._onCancel}
|
||||||
focus={false}
|
focus={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
{_t(
|
{_t(
|
||||||
"If you've forgotten your recovery passphrase you can "+
|
"If you've forgotten your recovery passphrase you can "+
|
||||||
"<button1>use your recovery key</button1> or " +
|
"<button1>use your recovery key</button1> or " +
|
||||||
|
@ -371,7 +361,6 @@ export default class RestoreKeyBackupDialog extends React.PureComponent {
|
||||||
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
<div className="mx_RestoreKeyBackupDialog_primaryContainer">
|
||||||
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
<input className="mx_RestoreKeyBackupDialog_recoveryKeyInput"
|
||||||
onChange={this._onRecoveryKeyChange}
|
onChange={this._onRecoveryKeyChange}
|
||||||
onKeyPress={this._onRecoveryKeyKeyPress}
|
|
||||||
value={this.state.recoveryKey}
|
value={this.state.recoveryKey}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018, 2019 New Vector Ltd
|
Copyright 2018, 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -21,7 +21,6 @@ import * as sdk from '../../../../index';
|
||||||
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../../MatrixClientPeg';
|
||||||
|
|
||||||
import { _t } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
import { Key } from "../../../../Keyboard";
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Access Secure Secret Storage by requesting the user's passphrase.
|
* Access Secure Secret Storage by requesting the user's passphrase.
|
||||||
|
@ -68,7 +67,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseNext = async () => {
|
_onPassPhraseNext = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (this.state.passPhrase.length <= 0) return;
|
||||||
|
|
||||||
this.setState({ keyMatches: null });
|
this.setState({ keyMatches: null });
|
||||||
const input = { passphrase: this.state.passPhrase };
|
const input = { passphrase: this.state.passPhrase };
|
||||||
const keyMatches = await this.props.checkPrivateKey(input);
|
const keyMatches = await this.props.checkPrivateKey(input);
|
||||||
|
@ -79,7 +82,11 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRecoveryKeyNext = async () => {
|
_onRecoveryKeyNext = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!this.state.recoveryKeyValid) return;
|
||||||
|
|
||||||
this.setState({ keyMatches: null });
|
this.setState({ keyMatches: null });
|
||||||
const input = { recoveryKey: this.state.recoveryKey };
|
const input = { recoveryKey: this.state.recoveryKey };
|
||||||
const keyMatches = await this.props.checkPrivateKey(input);
|
const keyMatches = await this.props.checkPrivateKey(input);
|
||||||
|
@ -97,18 +104,6 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPassPhraseKeyPress = (e) => {
|
|
||||||
if (e.key === Key.ENTER && this.state.passPhrase.length > 0) {
|
|
||||||
this._onPassPhraseNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_onRecoveryKeyKeyPress = (e) => {
|
|
||||||
if (e.key === Key.ENTER && this.state.recoveryKeyValid) {
|
|
||||||
this._onRecoveryKeyNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
@ -135,7 +130,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
content = <div>
|
content = <div>
|
||||||
|
@ -149,23 +144,25 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
"identity for verifying other sessions by entering your passphrase.",
|
"identity for verifying other sessions by entering your passphrase.",
|
||||||
)}</p>
|
)}</p>
|
||||||
|
|
||||||
<div className="mx_AccessSecretStorageDialog_primaryContainer">
|
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onPassPhraseNext}>
|
||||||
<input type="password"
|
<input
|
||||||
|
type="password"
|
||||||
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
className="mx_AccessSecretStorageDialog_passPhraseInput"
|
||||||
onChange={this._onPassPhraseChange}
|
onChange={this._onPassPhraseChange}
|
||||||
onKeyPress={this._onPassPhraseKeyPress}
|
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
{keyStatus}
|
{keyStatus}
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onPassPhraseNext}
|
onPrimaryButtonClick={this._onPassPhraseNext}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onCancel={this._onCancel}
|
onCancel={this._onCancel}
|
||||||
focus={false}
|
focus={false}
|
||||||
primaryDisabled={this.state.passPhrase.length === 0}
|
primaryDisabled={this.state.passPhrase.length === 0}
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
{_t(
|
{_t(
|
||||||
"If you've forgotten your passphrase you can "+
|
"If you've forgotten your passphrase you can "+
|
||||||
"<button1>use your recovery key</button1> or " +
|
"<button1>use your recovery key</button1> or " +
|
||||||
|
@ -192,11 +189,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
let keyStatus;
|
let keyStatus;
|
||||||
if (this.state.recoveryKey.length === 0) {
|
if (this.state.recoveryKey.length === 0) {
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus"></div>;
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus" />;
|
||||||
} else if (this.state.recoveryKeyValid) {
|
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
|
||||||
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
|
||||||
</div>;
|
|
||||||
} else if (this.state.keyMatches === false) {
|
} else if (this.state.keyMatches === false) {
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
{"\uD83D\uDC4E "}{_t(
|
{"\uD83D\uDC4E "}{_t(
|
||||||
|
@ -204,6 +197,10 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
"entered the correct recovery key.",
|
"entered the correct recovery key.",
|
||||||
)}
|
)}
|
||||||
</div>;
|
</div>;
|
||||||
|
} else if (this.state.recoveryKeyValid) {
|
||||||
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
|
{"\uD83D\uDC4D "}{_t("This looks like a valid recovery key!")}
|
||||||
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
keyStatus = <div className="mx_AccessSecretStorageDialog_keyStatus">
|
||||||
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
{"\uD83D\uDC4E "}{_t("Not a valid recovery key")}
|
||||||
|
@ -221,22 +218,22 @@ export default class AccessSecretStorageDialog extends React.PureComponent {
|
||||||
"identity for verifying other sessions by entering your recovery key.",
|
"identity for verifying other sessions by entering your recovery key.",
|
||||||
)}</p>
|
)}</p>
|
||||||
|
|
||||||
<div className="mx_AccessSecretStorageDialog_primaryContainer">
|
<form className="mx_AccessSecretStorageDialog_primaryContainer" onSubmit={this._onRecoveryKeyNext}>
|
||||||
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
|
<input className="mx_AccessSecretStorageDialog_recoveryKeyInput"
|
||||||
onChange={this._onRecoveryKeyChange}
|
onChange={this._onRecoveryKeyChange}
|
||||||
onKeyPress={this._onRecoveryKeyKeyPress}
|
|
||||||
value={this.state.recoveryKey}
|
value={this.state.recoveryKey}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
{keyStatus}
|
{keyStatus}
|
||||||
<DialogButtons primaryButton={_t('Next')}
|
<DialogButtons
|
||||||
|
primaryButton={_t('Next')}
|
||||||
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
onPrimaryButtonClick={this._onRecoveryKeyNext}
|
||||||
hasCancel={true}
|
hasCancel={true}
|
||||||
onCancel={this._onCancel}
|
onCancel={this._onCancel}
|
||||||
focus={false}
|
focus={false}
|
||||||
primaryDisabled={!this.state.recoveryKeyValid}
|
primaryDisabled={!this.state.recoveryKeyValid}
|
||||||
/>
|
/>
|
||||||
</div>
|
</form>
|
||||||
{_t(
|
{_t(
|
||||||
"If you've forgotten your recovery key you can "+
|
"If you've forgotten your recovery key you can "+
|
||||||
"<button>set up new recovery options</button>."
|
"<button>set up new recovery options</button>."
|
||||||
|
|
|
@ -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.
|
// Using Object.assign workaround as the following opens in a new window instead of a new tab.
|
||||||
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
|
// window.open(this._getSafeUrl(), '_blank', 'noopener=yes');
|
||||||
Object.assign(document.createElement('a'),
|
Object.assign(document.createElement('a'),
|
||||||
{ target: '_blank', href: this._getSafeUrl(), rel: 'noopener'}).click();
|
{ target: '_blank', href: this._getSafeUrl(), rel: 'noreferrer noopener'}).click();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onReloadWidgetClick() {
|
_onReloadWidgetClick() {
|
||||||
|
|
|
@ -91,7 +91,7 @@ export default class ImageView extends React.Component {
|
||||||
getName() {
|
getName() {
|
||||||
let name = this.props.name;
|
let name = this.props.name;
|
||||||
if (name && this.props.link) {
|
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;
|
return name;
|
||||||
}
|
}
|
||||||
|
@ -216,7 +216,7 @@ export default class ImageView extends React.Component {
|
||||||
{ this.getName() }
|
{ this.getName() }
|
||||||
</div>
|
</div>
|
||||||
{ eventMeta }
|
{ eventMeta }
|
||||||
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } target="_blank" rel="noopener">
|
<a className="mx_ImageView_link" href={ this.props.src } download={ this.props.name } rel="noreferrer noopener">
|
||||||
<div className="mx_ImageView_download">
|
<div className="mx_ImageView_download">
|
||||||
{ _t('Download this file') }<br />
|
{ _t('Download this file') }<br />
|
||||||
<span className="mx_ImageView_size">{ sizeRes }</span>
|
<span className="mx_ImageView_size">{ sizeRes }</span>
|
||||||
|
|
|
@ -23,7 +23,6 @@ import classNames from 'classnames';
|
||||||
import { Room, RoomMember } from 'matrix-js-sdk';
|
import { Room, RoomMember } from 'matrix-js-sdk';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import { getDisplayAliasForRoom } from '../../../Rooms';
|
|
||||||
import FlairStore from "../../../stores/FlairStore";
|
import FlairStore from "../../../stores/FlairStore";
|
||||||
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
import {getPrimaryPermalinkEntity} from "../../../utils/permalinks/Permalinks";
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
|
@ -128,7 +127,8 @@ const Pill = createReactClass({
|
||||||
case Pill.TYPE_ROOM_MENTION: {
|
case Pill.TYPE_ROOM_MENTION: {
|
||||||
const localRoom = resourceId[0] === '#' ?
|
const localRoom = resourceId[0] === '#' ?
|
||||||
MatrixClientPeg.get().getRooms().find((r) => {
|
MatrixClientPeg.get().getRooms().find((r) => {
|
||||||
return r.getAliases().includes(resourceId);
|
return r.getCanonicalAlias() === resourceId ||
|
||||||
|
r.getAltAliases().includes(resourceId);
|
||||||
}) : MatrixClientPeg.get().getRoom(resourceId);
|
}) : MatrixClientPeg.get().getRoom(resourceId);
|
||||||
room = localRoom;
|
room = localRoom;
|
||||||
if (!localRoom) {
|
if (!localRoom) {
|
||||||
|
@ -211,7 +211,7 @@ const Pill = createReactClass({
|
||||||
if (room) {
|
if (room) {
|
||||||
linkText = "@room";
|
linkText = "@room";
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_AtRoomPill';
|
pillClass = 'mx_AtRoomPill';
|
||||||
}
|
}
|
||||||
|
@ -225,7 +225,7 @@ const Pill = createReactClass({
|
||||||
member.rawDisplayName = member.rawDisplayName || '';
|
member.rawDisplayName = member.rawDisplayName || '';
|
||||||
linkText = member.rawDisplayName;
|
linkText = member.rawDisplayName;
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <MemberAvatar member={member} width={16} height={16} />;
|
avatar = <MemberAvatar member={member} width={16} height={16} aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_UserPill';
|
pillClass = 'mx_UserPill';
|
||||||
href = null;
|
href = null;
|
||||||
|
@ -236,12 +236,12 @@ const Pill = createReactClass({
|
||||||
case Pill.TYPE_ROOM_MENTION: {
|
case Pill.TYPE_ROOM_MENTION: {
|
||||||
const room = this.state.room;
|
const room = this.state.room;
|
||||||
if (room) {
|
if (room) {
|
||||||
linkText = (room ? getDisplayAliasForRoom(room) : null) || resource;
|
linkText = resource;
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <RoomAvatar room={room} width={16} height={16} />;
|
avatar = <RoomAvatar room={room} width={16} height={16} aria-hidden="true" />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_RoomPill';
|
|
||||||
}
|
}
|
||||||
|
pillClass = 'mx_RoomPill';
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case Pill.TYPE_GROUP_MENTION: {
|
case Pill.TYPE_GROUP_MENTION: {
|
||||||
|
@ -251,7 +251,7 @@ const Pill = createReactClass({
|
||||||
|
|
||||||
linkText = groupId;
|
linkText = groupId;
|
||||||
if (this.props.shouldShowPillAvatar) {
|
if (this.props.shouldShowPillAvatar) {
|
||||||
avatar = <BaseAvatar name={name || groupId} width={16} height={16}
|
avatar = <BaseAvatar name={name || groupId} width={16} height={16} aria-hidden="true"
|
||||||
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
url={avatarUrl ? cli.mxcUrlToHttp(avatarUrl, 16, 16) : null} />;
|
||||||
}
|
}
|
||||||
pillClass = 'mx_GroupPill';
|
pillClass = 'mx_GroupPill';
|
||||||
|
|
|
@ -17,40 +17,151 @@ limitations under the License.
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import PropTypes from "prop-types";
|
import PropTypes from "prop-types";
|
||||||
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
import {replaceableComponent} from "../../../../utils/replaceableComponent";
|
||||||
import * as qs from "qs";
|
import {MatrixClientPeg} from "../../../../MatrixClientPeg";
|
||||||
import QRCode from "qrcode-react";
|
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")
|
@replaceableComponent("views.elements.crypto.VerificationQRCode")
|
||||||
export default class VerificationQRCode extends React.PureComponent {
|
export default class VerificationQRCode extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
// Common for all kinds of QR codes
|
prefix: PropTypes.string.isRequired,
|
||||||
keys: PropTypes.array.isRequired, // array of [Key ID, Base64 Key] pairs
|
version: PropTypes.number.isRequired,
|
||||||
action: PropTypes.string.isRequired,
|
mode: PropTypes.number.isRequired,
|
||||||
keyholderUserId: PropTypes.string.isRequired,
|
transactionId: PropTypes.string.isRequired, // or requestEventId
|
||||||
|
firstKeyB64: PropTypes.string.isRequired,
|
||||||
// User verification use case only
|
secondKeyB64: PropTypes.string.isRequired,
|
||||||
secret: PropTypes.string,
|
secretB64: PropTypes.string.isRequired,
|
||||||
otherUserKey: PropTypes.string, // Base64 key being verified
|
|
||||||
requestEventId: PropTypes.string,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
static defaultProps = {
|
static async getPropsForRequest(verificationRequest: VerificationRequest) {
|
||||||
action: "verify",
|
const cli = MatrixClientPeg.get();
|
||||||
};
|
const myUserId = cli.getUserId();
|
||||||
|
const otherUserId = verificationRequest.otherUserId;
|
||||||
|
|
||||||
render() {
|
let mode = MODE_VERIFY_OTHER_USER;
|
||||||
const query = {
|
if (myUserId === otherUserId) {
|
||||||
request: this.props.requestEventId,
|
// Mode changes depending on whether or not we trust the master cross signing key
|
||||||
action: this.props.action,
|
const myTrust = cli.checkUserTrust(myUserId);
|
||||||
other_user_key: this.props.otherUserKey,
|
if (myTrust.isCrossSigningVerified()) {
|
||||||
secret: this.props.secret,
|
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||||
};
|
} else {
|
||||||
for (const key of this.props.keys) {
|
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||||
query[`key_${key[0]}`] = key[1];
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const uri = `https://matrix.to/#/${this.props.keyholderUserId}?${qs.stringify(query)}`;
|
const requestEvent = verificationRequest.requestEvent;
|
||||||
|
const transactionId = requestEvent.getId()
|
||||||
|
? requestEvent.getId()
|
||||||
|
: ToDeviceChannel.getTransactionId(requestEvent);
|
||||||
|
|
||||||
return <QRCode value={uri} size={512} logoWidth={64} logo={require("../../../../../res/img/matrix-m.svg")} />;
|
const qrProps = {
|
||||||
|
prefix: BINARY_PREFIX,
|
||||||
|
version: CODE_VERSION,
|
||||||
|
mode,
|
||||||
|
transactionId,
|
||||||
|
firstKeyB64: '', // worked out shortly
|
||||||
|
secondKeyB64: '', // worked out shortly
|
||||||
|
secretB64: verificationRequest.encodedSharedSecret,
|
||||||
|
};
|
||||||
|
|
||||||
|
const myCrossSigningInfo = cli.getStoredCrossSigningForUser(myUserId);
|
||||||
|
const myDevices = (await cli.getStoredDevicesForUser(myUserId)) || [];
|
||||||
|
|
||||||
|
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||||
|
// First key is our master cross signing key
|
||||||
|
qrProps.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(4);
|
||||||
|
tmpBuf.writeInt8(i, 0);
|
||||||
|
buf = Buffer.concat([buf, tmpBuf]);
|
||||||
|
};
|
||||||
|
const appendStr = (s: string, enc: string) => {
|
||||||
|
const tmpBuf = Buffer.from(s, enc);
|
||||||
|
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");
|
||||||
|
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() {
|
||||||
|
if (!this.state.dataUri) {
|
||||||
|
return <div className='mx_VerificationQRCode'><Spinner /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <img src={this.state.dataUri} className='mx_VerificationQRCode' />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
import { editBodyDiffToHtml } from '../../../utils/MessageDiffUtils';
|
||||||
import {formatTime} from '../../../DateUtils';
|
import {formatTime} from '../../../DateUtils';
|
||||||
import {MatrixEvent} from 'matrix-js-sdk';
|
import {MatrixEvent} from 'matrix-js-sdk';
|
||||||
import {pillifyLinks} from '../../../utils/pillify';
|
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
|
@ -53,6 +53,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||||
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
this.state = {canRedact, sendStatus: event.getAssociatedStatus()};
|
||||||
|
|
||||||
this._content = createRef();
|
this._content = createRef();
|
||||||
|
this._pills = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAssociatedStatusChanged = () => {
|
_onAssociatedStatusChanged = () => {
|
||||||
|
@ -81,7 +82,7 @@ export default class EditHistoryMessage extends React.PureComponent {
|
||||||
pillifyLinks() {
|
pillifyLinks() {
|
||||||
// not present for redacted events
|
// not present for redacted events
|
||||||
if (this._content.current) {
|
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() {
|
componentWillUnmount() {
|
||||||
|
unmountPills(this._pills);
|
||||||
const event = this.props.mxEvent;
|
const event = this.props.mxEvent;
|
||||||
if (event.localRedactionEvent()) {
|
if (event.localRedactionEvent()) {
|
||||||
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
event.localRedactionEvent().off("status", this._onAssociatedStatusChanged);
|
||||||
|
|
|
@ -26,7 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
|
||||||
import Tinter from '../../../Tinter';
|
import Tinter from '../../../Tinter';
|
||||||
import request from 'browser-request';
|
import request from 'browser-request';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import SdkConfig from "../../../SdkConfig";
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
|
||||||
|
|
||||||
// A cached tinted copy of require("../../../../res/img/download.svg")
|
// A cached tinted copy of require("../../../../res/img/download.svg")
|
||||||
|
@ -94,84 +94,6 @@ Tinter.registerTintable(updateTintedDownloadImage);
|
||||||
// The downside of using a second domain is that it complicates hosting,
|
// The downside of using a second domain is that it complicates hosting,
|
||||||
// the downside of using a sandboxed iframe is that the browers are overly
|
// the downside of using a sandboxed iframe is that the browers are overly
|
||||||
// restrictive in what you are allowed to do with the generated URL.
|
// restrictive in what you are allowed to do with the generated URL.
|
||||||
//
|
|
||||||
// For now given how unusable the blobs generated in sandboxed iframes are we
|
|
||||||
// default to using a renderer hosted on "usercontent.riot.im". This is
|
|
||||||
// overridable so that people running their own version of the client can
|
|
||||||
// choose a different renderer.
|
|
||||||
//
|
|
||||||
// To that end the current version of the blob generation is the following
|
|
||||||
// html:
|
|
||||||
//
|
|
||||||
// <html><head><script>
|
|
||||||
// var params = window.location.search.substring(1).split('&');
|
|
||||||
// var lockOrigin;
|
|
||||||
// for (var i = 0; i < params.length; ++i) {
|
|
||||||
// var parts = params[i].split('=');
|
|
||||||
// if (parts[0] == 'origin') lockOrigin = decodeURIComponent(parts[1]);
|
|
||||||
// }
|
|
||||||
// window.onmessage=function(e){
|
|
||||||
// if (lockOrigin === undefined || e.origin === lockOrigin) eval("("+e.data.code+")")(e);
|
|
||||||
// }
|
|
||||||
// </script></head><body></body></html>
|
|
||||||
//
|
|
||||||
// This waits to receive a message event sent using the window.postMessage API.
|
|
||||||
// When it receives the event it evals a javascript function in data.code and
|
|
||||||
// runs the function passing the event as an argument. This version adds
|
|
||||||
// support for a query parameter controlling the origin from which messages
|
|
||||||
// will be processed as an extra layer of security (note that the default URL
|
|
||||||
// is still 'v1' since it is backwards compatible).
|
|
||||||
//
|
|
||||||
// In particular it means that the rendering function can be written as a
|
|
||||||
// ordinary javascript function which then is turned into a string using
|
|
||||||
// toString().
|
|
||||||
//
|
|
||||||
const DEFAULT_CROSS_ORIGIN_RENDERER = "https://usercontent.riot.im/v1.html";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Render the attachment inside the iframe.
|
|
||||||
* We can't use imported libraries here so this has to be vanilla JS.
|
|
||||||
*/
|
|
||||||
function remoteRender(event) {
|
|
||||||
const data = event.data;
|
|
||||||
|
|
||||||
const img = document.createElement("img");
|
|
||||||
img.id = "img";
|
|
||||||
img.src = data.imgSrc;
|
|
||||||
|
|
||||||
const a = document.createElement("a");
|
|
||||||
a.id = "a";
|
|
||||||
a.rel = data.rel;
|
|
||||||
a.target = data.target;
|
|
||||||
a.download = data.download;
|
|
||||||
a.style = data.style;
|
|
||||||
a.style.fontFamily = "Arial, Helvetica, Sans-Serif";
|
|
||||||
a.href = window.URL.createObjectURL(data.blob);
|
|
||||||
a.appendChild(img);
|
|
||||||
a.appendChild(document.createTextNode(data.textContent));
|
|
||||||
|
|
||||||
const body = document.body;
|
|
||||||
// Don't display scrollbars if the link takes more than one line
|
|
||||||
// to display.
|
|
||||||
body.style = "margin: 0px; overflow: hidden";
|
|
||||||
body.appendChild(a);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the tint inside the iframe.
|
|
||||||
* We can't use imported libraries here so this has to be vanilla JS.
|
|
||||||
*/
|
|
||||||
function remoteSetTint(event) {
|
|
||||||
const data = event.data;
|
|
||||||
|
|
||||||
const img = document.getElementById("img");
|
|
||||||
img.src = data.imgSrc;
|
|
||||||
img.style = data.imgStyle;
|
|
||||||
|
|
||||||
const a = document.getElementById("a");
|
|
||||||
a.style = data.style;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current CSS style for a DOMElement.
|
* Get the current CSS style for a DOMElement.
|
||||||
|
@ -283,7 +205,6 @@ export default createReactClass({
|
||||||
// will be inside the iframe so we wont be able to update
|
// will be inside the iframe so we wont be able to update
|
||||||
// it directly.
|
// it directly.
|
||||||
this._iframe.current.contentWindow.postMessage({
|
this._iframe.current.contentWindow.postMessage({
|
||||||
code: remoteSetTint.toString(),
|
|
||||||
imgSrc: tintedDownloadImageURL,
|
imgSrc: tintedDownloadImageURL,
|
||||||
style: computedStyle(this._dummyLink.current),
|
style: computedStyle(this._dummyLink.current),
|
||||||
}, "*");
|
}, "*");
|
||||||
|
@ -306,7 +227,7 @@ export default createReactClass({
|
||||||
// Wait for the user to click on the link before downloading
|
// Wait for the user to click on the link before downloading
|
||||||
// and decrypting the attachment.
|
// and decrypting the attachment.
|
||||||
let decrypting = false;
|
let decrypting = false;
|
||||||
const decrypt = () => {
|
const decrypt = (e) => {
|
||||||
if (decrypting) {
|
if (decrypting) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -323,16 +244,15 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
decrypting = false;
|
decrypting = false;
|
||||||
return;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
<a href="javascript:void(0)" onClick={decrypt}>
|
<AccessibleButton onClick={decrypt}>
|
||||||
{ _t("Decrypt %(text)s", { text: text }) }
|
{ _t("Decrypt %(text)s", { text: text }) }
|
||||||
</a>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
@ -341,7 +261,6 @@ export default createReactClass({
|
||||||
// When the iframe loads we tell it to render a download link
|
// When the iframe loads we tell it to render a download link
|
||||||
const onIframeLoad = (ev) => {
|
const onIframeLoad = (ev) => {
|
||||||
ev.target.contentWindow.postMessage({
|
ev.target.contentWindow.postMessage({
|
||||||
code: remoteRender.toString(),
|
|
||||||
imgSrc: tintedDownloadImageURL,
|
imgSrc: tintedDownloadImageURL,
|
||||||
style: computedStyle(this._dummyLink.current),
|
style: computedStyle(this._dummyLink.current),
|
||||||
blob: this.state.decryptedBlob,
|
blob: this.state.decryptedBlob,
|
||||||
|
@ -349,19 +268,13 @@ export default createReactClass({
|
||||||
// will have the correct name when the user tries to download it.
|
// will have the correct name when the user tries to download it.
|
||||||
// We can't provide a Content-Disposition header like we would for HTTP.
|
// We can't provide a Content-Disposition header like we would for HTTP.
|
||||||
download: fileName,
|
download: fileName,
|
||||||
rel: "noopener",
|
|
||||||
target: "_blank",
|
|
||||||
textContent: _t("Download %(text)s", { text: text }),
|
textContent: _t("Download %(text)s", { text: text }),
|
||||||
}, "*");
|
}, "*");
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the attachment is encryped then put the link inside an iframe.
|
const url = "usercontent/"; // XXX: this path should probably be passed from the skin
|
||||||
let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
|
|
||||||
const appConfig = SdkConfig.get();
|
// If the attachment is encrypted then put the link inside an iframe.
|
||||||
if (appConfig && appConfig.cross_origin_renderer_url) {
|
|
||||||
renderer_url = appConfig.cross_origin_renderer_url;
|
|
||||||
}
|
|
||||||
renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
|
|
||||||
return (
|
return (
|
||||||
<span className="mx_MFileBody">
|
<span className="mx_MFileBody">
|
||||||
<div className="mx_MFileBody_download">
|
<div className="mx_MFileBody_download">
|
||||||
|
@ -373,14 +286,18 @@ export default createReactClass({
|
||||||
*/ }
|
*/ }
|
||||||
<a ref={this._dummyLink} />
|
<a ref={this._dummyLink} />
|
||||||
</div>
|
</div>
|
||||||
<iframe src={renderer_url} onLoad={onIframeLoad} ref={this._iframe} />
|
<iframe
|
||||||
|
src={`${url}?origin=${encodeURIComponent(window.location.origin)}`}
|
||||||
|
onLoad={onIframeLoad}
|
||||||
|
ref={this._iframe}
|
||||||
|
sandbox="allow-scripts allow-downloads" />
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
} else if (contentUrl) {
|
} else if (contentUrl) {
|
||||||
const downloadProps = {
|
const downloadProps = {
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
rel: "noopener",
|
rel: "noreferrer noopener",
|
||||||
|
|
||||||
// We set the href regardless of whether or not we intercept the download
|
// 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
|
// because we don't really want to convert the file to a blob eagerly, and
|
||||||
|
|
|
@ -40,7 +40,10 @@ export default class MKeyVerificationConclusion extends React.Component {
|
||||||
if (request) {
|
if (request) {
|
||||||
request.off("change", this._onRequestChanged);
|
request.off("change", this._onRequestChanged);
|
||||||
}
|
}
|
||||||
MatrixClientPeg.removeListener("userTrustStatusChanged", this._onTrustChanged);
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (cli) {
|
||||||
|
cli.removeListener("userTrustStatusChanged", this._onTrustChanged);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRequestChanged = () => {
|
_onRequestChanged = () => {
|
||||||
|
|
|
@ -27,6 +27,7 @@ import {RIGHT_PANEL_PHASES} from "../../../stores/RightPanelStorePhases";
|
||||||
export default class MKeyVerificationRequest extends React.Component {
|
export default class MKeyVerificationRequest extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
this.state = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -93,10 +94,20 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
_cancelledLabel(userId) {
|
_cancelledLabel(userId) {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const myUserId = client.getUserId();
|
const myUserId = client.getUserId();
|
||||||
|
const {cancellationCode} = this.props.mxEvent.verificationRequest;
|
||||||
|
const declined = cancellationCode === "m.user";
|
||||||
if (userId === myUserId) {
|
if (userId === myUserId) {
|
||||||
return _t("You cancelled");
|
if (declined) {
|
||||||
|
return _t("You declined");
|
||||||
|
} else {
|
||||||
|
return _t("You cancelled");
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
|
if (declined) {
|
||||||
|
return _t("%(name)s declined", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
|
||||||
|
} else {
|
||||||
|
return _t("%(name)s cancelled", {name: getNameForEventRoom(userId, this.props.mxEvent.getRoomId())});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,15 +126,19 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
let subtitle;
|
let subtitle;
|
||||||
let stateNode;
|
let stateNode;
|
||||||
|
|
||||||
const accepted = request.ready || request.started || request.done;
|
if (!request.canAccept) {
|
||||||
if (accepted || request.cancelled) {
|
|
||||||
let stateLabel;
|
let stateLabel;
|
||||||
|
const accepted = request.ready || request.started || request.done;
|
||||||
if (accepted) {
|
if (accepted) {
|
||||||
stateLabel = (<AccessibleButton onClick={this._openRequest}>
|
stateLabel = (<AccessibleButton onClick={this._openRequest}>
|
||||||
{this._acceptedLabel(request.receivingUserId)}
|
{this._acceptedLabel(request.receivingUserId)}
|
||||||
</AccessibleButton>);
|
</AccessibleButton>);
|
||||||
} else {
|
} else if (request.cancelled) {
|
||||||
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
stateLabel = this._cancelledLabel(request.cancellingUserId);
|
||||||
|
} else if (request.accepting) {
|
||||||
|
stateLabel = _t("accepting …");
|
||||||
|
} else if (request.declining) {
|
||||||
|
stateLabel = _t("declining …");
|
||||||
}
|
}
|
||||||
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
|
stateNode = (<div className="mx_cryptoEvent_state">{stateLabel}</div>);
|
||||||
}
|
}
|
||||||
|
@ -134,7 +149,7 @@ export default class MKeyVerificationRequest extends React.Component {
|
||||||
_t("%(name)s wants to verify", {name})}</div>);
|
_t("%(name)s wants to verify", {name})}</div>);
|
||||||
subtitle = (<div className="mx_cryptoEvent_subtitle">{
|
subtitle = (<div className="mx_cryptoEvent_subtitle">{
|
||||||
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
|
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}</div>);
|
||||||
if (request.requested && !request.observeOnly) {
|
if (request.canAccept) {
|
||||||
stateNode = (<div className="mx_cryptoEvent_buttons">
|
stateNode = (<div className="mx_cryptoEvent_buttons">
|
||||||
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
<FormButton kind="danger" onClick={this._onRejectClicked} label={_t("Decline")} />
|
||||||
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
|
<FormButton onClick={this._onAcceptClicked} label={_t("Accept")} />
|
||||||
|
|
|
@ -30,7 +30,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import * as ContextMenu from '../../structures/ContextMenu';
|
import * as ContextMenu from '../../structures/ContextMenu';
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {pillifyLinks} from '../../../utils/pillify';
|
import {pillifyLinks, unmountPills} from '../../../utils/pillify';
|
||||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||||
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
import {isPermalinkHost} from "../../../utils/permalinks/Permalinks";
|
||||||
import {toRightOf} from "../../structures/ContextMenu";
|
import {toRightOf} from "../../structures/ContextMenu";
|
||||||
|
@ -92,6 +92,7 @@ export default createReactClass({
|
||||||
|
|
||||||
componentDidMount: function() {
|
componentDidMount: function() {
|
||||||
this._unmounted = false;
|
this._unmounted = false;
|
||||||
|
this._pills = [];
|
||||||
if (!this.props.editState) {
|
if (!this.props.editState) {
|
||||||
this._applyFormatting();
|
this._applyFormatting();
|
||||||
}
|
}
|
||||||
|
@ -103,7 +104,7 @@ export default createReactClass({
|
||||||
// pillifyLinks BEFORE linkifyElement because plain room/user URLs in the composer
|
// 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,
|
// 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.
|
// 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);
|
HtmlUtils.linkifyElement(this._content.current);
|
||||||
this.calculateUrlPreview();
|
this.calculateUrlPreview();
|
||||||
|
|
||||||
|
@ -146,6 +147,7 @@ export default createReactClass({
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
componentWillUnmount: function() {
|
||||||
this._unmounted = true;
|
this._unmounted = true;
|
||||||
|
unmountPills(this._pills);
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
|
@ -372,7 +374,9 @@ export default createReactClass({
|
||||||
const height = window.screen.height > 800 ? 800 : window.screen.height;
|
const height = window.screen.height > 800 ? 800 : window.screen.height;
|
||||||
const left = (window.screen.width - width) / 2;
|
const left = (window.screen.width - width) / 2;
|
||||||
const top = (window.screen.height - height) / 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;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -23,20 +23,23 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
||||||
import {ensureDMExists} from "../../../createRoom";
|
import {ensureDMExists} from "../../../createRoom";
|
||||||
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
import {useEventEmitter} from "../../../hooks/useEventEmitter";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import {PHASE_REQUESTED} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
import {PHASE_REQUESTED, PHASE_UNSENT} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import * as sdk from "../../../index";
|
import * as sdk from "../../../index";
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
|
|
||||||
// cancellation codes which constitute a key mismatch
|
// cancellation codes which constitute a key mismatch
|
||||||
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
|
||||||
|
|
||||||
const EncryptionPanel = ({verificationRequest, member, onClose}) => {
|
const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
|
||||||
const [request, setRequest] = useState(verificationRequest);
|
const [request, setRequest] = useState(verificationRequest);
|
||||||
useEffect(() => {
|
|
||||||
setRequest(verificationRequest);
|
|
||||||
}, [verificationRequest]);
|
|
||||||
|
|
||||||
const [phase, setPhase] = useState(request && request.phase);
|
const [phase, setPhase] = useState(request && request.phase);
|
||||||
|
useEffect(() => {
|
||||||
|
setRequest(verificationRequest);
|
||||||
|
if (verificationRequest) {
|
||||||
|
setPhase(verificationRequest.phase);
|
||||||
|
}
|
||||||
|
}, [verificationRequest]);
|
||||||
const changeHandler = useCallback(() => {
|
const changeHandler = useCallback(() => {
|
||||||
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
|
// handle transitions -> cancelled for mismatches which fire a modal instead of showing a card
|
||||||
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
|
if (request && request.cancelled && MISMATCHES.includes(request.cancellationCode)) {
|
||||||
|
@ -69,14 +72,16 @@ const EncryptionPanel = ({verificationRequest, member, onClose}) => {
|
||||||
const roomId = await ensureDMExists(cli, member.userId);
|
const roomId = await ensureDMExists(cli, member.userId);
|
||||||
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
|
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
|
||||||
setRequest(verificationRequest);
|
setRequest(verificationRequest);
|
||||||
|
setPhase(verificationRequest.phase);
|
||||||
}, [member.userId]);
|
}, [member.userId]);
|
||||||
|
|
||||||
const requested = request && (phase === PHASE_REQUESTED || phase === undefined);
|
const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
|
||||||
if (!request || requested) {
|
if (!request || requested) {
|
||||||
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
|
return <EncryptionInfo onStartVerification={onStartVerification} member={member} pending={requested} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<VerificationPanel
|
<VerificationPanel
|
||||||
|
layout={layout}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
member={member}
|
member={member}
|
||||||
request={request}
|
request={request}
|
||||||
|
@ -89,6 +94,7 @@ EncryptionPanel.propTypes = {
|
||||||
member: PropTypes.object.isRequired,
|
member: PropTypes.object.isRequired,
|
||||||
onClose: PropTypes.func.isRequired,
|
onClose: PropTypes.func.isRequired,
|
||||||
verificationRequest: PropTypes.object,
|
verificationRequest: PropTypes.object,
|
||||||
|
layout: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EncryptionPanel;
|
export default EncryptionPanel;
|
||||||
|
|
|
@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import createRoom from '../../../createRoom';
|
import createRoom, {findDMForUser} from '../../../createRoom';
|
||||||
import DMRoomMap from '../../../utils/DMRoomMap';
|
import DMRoomMap from '../../../utils/DMRoomMap';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
|
@ -135,19 +135,53 @@ function useIsEncrypted(cli, room) {
|
||||||
return isEncrypted;
|
return isEncrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyDevice(userId, device) {
|
async function verifyDevice(userId, device) {
|
||||||
const DeviceVerifyDialog = sdk.getComponent('views.dialogs.DeviceVerifyDialog');
|
const cli = MatrixClientPeg.get();
|
||||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
const member = cli.getUser(userId);
|
||||||
userId: userId,
|
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||||
device: device,
|
Modal.createTrackedDialog("Verification warning", "unverified session", QuestionDialog, {
|
||||||
}, null, /* priority = */ false, /* static = */ true);
|
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) {
|
function verifyUser(user) {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
const dmRoom = findDMForUser(cli, user.userId);
|
||||||
|
let existingRequest;
|
||||||
|
if (dmRoom) {
|
||||||
|
existingRequest = cli.findVerificationRequestDMInProgress(dmRoom.roomId);
|
||||||
|
}
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "set_right_panel_phase",
|
action: "set_right_panel_phase",
|
||||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||||
refireParams: {member: user},
|
refireParams: {
|
||||||
|
member: user,
|
||||||
|
verificationRequest: existingRequest,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,9 @@ import PropTypes from "prop-types";
|
||||||
|
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
import {verificationMethods} from 'matrix-js-sdk/src/crypto';
|
||||||
|
import {SCAN_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
|
|
||||||
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
||||||
import {MatrixClientPeg} from "../../../MatrixClientPeg";
|
|
||||||
import {_t} from "../../../languageHandler";
|
import {_t} from "../../../languageHandler";
|
||||||
import E2EIcon from "../rooms/E2EIcon";
|
import E2EIcon from "../rooms/E2EIcon";
|
||||||
import {
|
import {
|
||||||
|
@ -29,12 +30,13 @@ import {
|
||||||
PHASE_READY,
|
PHASE_READY,
|
||||||
PHASE_DONE,
|
PHASE_DONE,
|
||||||
PHASE_STARTED,
|
PHASE_STARTED,
|
||||||
PHASE_CANCELLED,
|
PHASE_CANCELLED, VerificationRequest,
|
||||||
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
import Spinner from "../elements/Spinner";
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
export default class VerificationPanel extends React.PureComponent {
|
export default class VerificationPanel extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
layout: PropTypes.string,
|
||||||
request: PropTypes.object.isRequired,
|
request: PropTypes.object.isRequired,
|
||||||
member: PropTypes.object.isRequired,
|
member: PropTypes.object.isRequired,
|
||||||
phase: PropTypes.oneOf([
|
phase: PropTypes.oneOf([
|
||||||
|
@ -50,68 +52,122 @@ export default class VerificationPanel extends React.PureComponent {
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {};
|
this.state = {
|
||||||
|
qrCodeProps: null, // generated by the VerificationQRCode component itself
|
||||||
|
};
|
||||||
this._hasVerifier = false;
|
this._hasVerifier = false;
|
||||||
|
if (this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD)) {
|
||||||
|
this._generateQRCodeProps(props.request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _generateQRCodeProps(verificationRequest: VerificationRequest) {
|
||||||
|
try {
|
||||||
|
this.setState({qrCodeProps: await VerificationQRCode.getPropsForRequest(verificationRequest)});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// Do nothing - we won't render a QR code.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderQRPhase(pending) {
|
renderQRPhase(pending) {
|
||||||
const {member, request} = this.props;
|
const {member, request} = this.props;
|
||||||
|
const showSAS = request.methods.includes(verificationMethods.SAS);
|
||||||
|
const showQR = this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD);
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
||||||
let button;
|
const noCommonMethodError = !showSAS && !showQR ?
|
||||||
if (pending) {
|
<p>{_t("The session you are trying to verify doesn't support scanning a QR code or emoji verification, which is what Riot supports. Try with a different client.")}</p> :
|
||||||
button = <Spinner />;
|
null;
|
||||||
} else {
|
|
||||||
button = (
|
if (this.props.layout === 'dialog') {
|
||||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
|
// HACK: This is a terrible idea.
|
||||||
{_t("Verify by emoji")}
|
let qrBlock;
|
||||||
</AccessibleButton>
|
let sasBlock;
|
||||||
|
if (showQR) {
|
||||||
|
let qrCode;
|
||||||
|
if (this.state.qrCodeProps) {
|
||||||
|
qrCode = <VerificationQRCode {...this.state.qrCodeProps} />;
|
||||||
|
} else {
|
||||||
|
qrCode = <div className='mx_VerificationPanel_QRPhase_noQR'><Spinner /></div>;
|
||||||
|
}
|
||||||
|
qrBlock =
|
||||||
|
<div className='mx_VerificationPanel_QRPhase_startOption'>
|
||||||
|
<p>{_t("Scan this unique code")}</p>
|
||||||
|
{qrCode}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
if (showSAS) {
|
||||||
|
sasBlock =
|
||||||
|
<div className='mx_VerificationPanel_QRPhase_startOption'>
|
||||||
|
<p>{_t("Compare unique emoji")}</p>
|
||||||
|
<span className='mx_VerificationPanel_QRPhase_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
|
||||||
|
<AccessibleButton disabled={this.state.emojiButtonClicked} onClick={this._startSAS} kind='primary'>
|
||||||
|
{_t("Start")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
const or = qrBlock && sasBlock ?
|
||||||
|
<div className='mx_VerificationPanel_QRPhase_betweenText'>{_t("or")}</div> : null;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{_t("Verify this session by completing one of the following:")}
|
||||||
|
<div className='mx_VerificationPanel_QRPhase_startOptions'>
|
||||||
|
{qrBlock}
|
||||||
|
{or}
|
||||||
|
{sasBlock}
|
||||||
|
{noCommonMethodError}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cli = MatrixClientPeg.get();
|
let qrBlock;
|
||||||
const crossSigningInfo = cli.getStoredCrossSigningForUser(request.otherUserId);
|
if (this.state.qrCodeProps) {
|
||||||
if (!crossSigningInfo || !request.requestEvent || !request.requestEvent.getId()) {
|
qrBlock = <div className="mx_UserInfo_container">
|
||||||
// for whatever reason we can't generate a QR code, offer only SAS Verification
|
<h3>{_t("Verify by scanning")}</h3>
|
||||||
return <div className="mx_UserInfo_container">
|
|
||||||
<h3>Verify by emoji</h3>
|
|
||||||
<p>{_t("Verify by comparing unique emoji.")}</p>
|
|
||||||
|
|
||||||
{ button }
|
|
||||||
</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const myKeyId = cli.getCrossSigningId();
|
|
||||||
const qrCodeKeys = [
|
|
||||||
[cli.getDeviceId(), cli.getDeviceEd25519Key()],
|
|
||||||
[myKeyId, myKeyId],
|
|
||||||
];
|
|
||||||
|
|
||||||
// TODO: add way to open camera to scan a QR code
|
|
||||||
return <React.Fragment>
|
|
||||||
<div className="mx_UserInfo_container">
|
|
||||||
<h3>Verify by scanning</h3>
|
|
||||||
<p>{_t("Ask %(displayName)s to scan your code:", {
|
<p>{_t("Ask %(displayName)s to scan your code:", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: member.displayName || member.name || member.userId,
|
||||||
})}</p>
|
})}</p>
|
||||||
|
|
||||||
<div className="mx_VerificationPanel_qrCode">
|
<div className="mx_VerificationPanel_qrCode">
|
||||||
<VerificationQRCode
|
<VerificationQRCode {...this.state.qrCodeProps} />
|
||||||
keyholderUserId={MatrixClientPeg.get().getUserId()}
|
|
||||||
requestEventId={request.requestEvent.getId()}
|
|
||||||
otherUserKey={crossSigningInfo.getId("master")}
|
|
||||||
secret={request.encodedSharedSecret}
|
|
||||||
keys={qrCodeKeys}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>;
|
||||||
|
}
|
||||||
<div className="mx_UserInfo_container">
|
|
||||||
<h3>Verify by emoji</h3>
|
|
||||||
<p>{_t("If you can't scan the code above, verify by comparing unique emoji.")}</p>
|
|
||||||
|
|
||||||
|
let sasBlock;
|
||||||
|
if (showSAS) {
|
||||||
|
let button;
|
||||||
|
if (pending) {
|
||||||
|
button = <Spinner />;
|
||||||
|
} else {
|
||||||
|
const disabled = this.state.emojiButtonClicked;
|
||||||
|
button = (
|
||||||
|
<AccessibleButton disabled={disabled} kind="primary" className="mx_UserInfo_wideButton" onClick={this._startSAS}>
|
||||||
|
{_t("Verify by emoji")}
|
||||||
|
</AccessibleButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const sasLabel = this.state.qrCodeProps ?
|
||||||
|
_t("If you can't scan the code above, verify by comparing unique emoji.") :
|
||||||
|
_t("Verify by comparing unique emoji.");
|
||||||
|
sasBlock = <div className="mx_UserInfo_container">
|
||||||
|
<h3>{_t("Verify by emoji")}</h3>
|
||||||
|
<p>{sasLabel}</p>
|
||||||
{ button }
|
{ button }
|
||||||
</div>
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const noCommonMethodBlock = noCommonMethodError ?
|
||||||
|
<div className="mx_UserInfo_container">{noCommonMethodError}</div> :
|
||||||
|
null;
|
||||||
|
|
||||||
|
// TODO: add way to open camera to scan a QR code
|
||||||
|
return <React.Fragment>
|
||||||
|
{qrBlock}
|
||||||
|
{sasBlock}
|
||||||
|
{noCommonMethodBlock}
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +181,7 @@ export default class VerificationPanel extends React.PureComponent {
|
||||||
<p>{_t("You've successfully verified %(displayName)s!", {
|
<p>{_t("You've successfully verified %(displayName)s!", {
|
||||||
displayName: member.displayName || member.name || member.userId,
|
displayName: member.displayName || member.name || member.userId,
|
||||||
})}</p>
|
})}</p>
|
||||||
<E2EIcon isUser={true} status="verified" size={128} />
|
<E2EIcon isUser={true} status="verified" size={128} hideTooltip={true} />
|
||||||
<p>Verify all users in a room to ensure it's secure.</p>
|
<p>Verify all users in a room to ensure it's secure.</p>
|
||||||
|
|
||||||
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
<AccessibleButton kind="primary" className="mx_UserInfo_wideButton" onClick={this.props.onClose}>
|
||||||
|
@ -196,6 +252,7 @@ export default class VerificationPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
_startSAS = async () => {
|
_startSAS = async () => {
|
||||||
|
this.setState({emojiButtonClicked: true});
|
||||||
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
|
const verifier = this.props.request.beginKeyVerification(verificationMethods.SAS);
|
||||||
try {
|
try {
|
||||||
await verifier.verify();
|
await verifier.verify();
|
||||||
|
@ -233,7 +290,11 @@ export default class VerificationPanel extends React.PureComponent {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.request.on("change", this._onRequestChange);
|
const {request} = this.props;
|
||||||
|
request.on("change", this._onRequestChange);
|
||||||
|
if (request.verifier) {
|
||||||
|
this.setState({sasEvent: request.verifier.sasEvent});
|
||||||
|
}
|
||||||
this._onRequestChange();
|
this._onRequestChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,6 @@ export default class AliasSettings extends React.Component {
|
||||||
roomId: PropTypes.string.isRequired,
|
roomId: PropTypes.string.isRequired,
|
||||||
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
canSetCanonicalAlias: PropTypes.bool.isRequired,
|
||||||
canSetAliases: PropTypes.bool.isRequired,
|
canSetAliases: PropTypes.bool.isRequired,
|
||||||
aliasEvents: PropTypes.array, // [MatrixEvent]
|
|
||||||
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
canonicalAliasEvent: PropTypes.object, // MatrixEvent
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -92,14 +91,9 @@ export default class AliasSettings extends React.Component {
|
||||||
remoteDomains: [], // [ domain.com, foobar.com ]
|
remoteDomains: [], // [ domain.com, foobar.com ]
|
||||||
canonicalAlias: null, // #canonical:domain.com
|
canonicalAlias: null, // #canonical:domain.com
|
||||||
updatingCanonicalAlias: false,
|
updatingCanonicalAlias: false,
|
||||||
|
localAliasesLoading: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const localDomain = MatrixClientPeg.get().getDomain();
|
|
||||||
state.domainToAliases = this.aliasEventsToDictionary(props.aliasEvents || []);
|
|
||||||
state.remoteDomains = Object.keys(state.domainToAliases).filter((domain) => {
|
|
||||||
return domain !== localDomain && state.domainToAliases[domain].length > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (props.canonicalAliasEvent) {
|
if (props.canonicalAliasEvent) {
|
||||||
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
|
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
|
||||||
}
|
}
|
||||||
|
@ -107,6 +101,46 @@ export default class AliasSettings extends React.Component {
|
||||||
this.state = state;
|
this.state = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async componentWillMount() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
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
|
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
|
||||||
const dict = {};
|
const dict = {};
|
||||||
aliasEvents.forEach((event) => {
|
aliasEvents.forEach((event) => {
|
||||||
|
@ -117,6 +151,16 @@ export default class AliasSettings extends React.Component {
|
||||||
return dict;
|
return dict;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_getAltAliases() {
|
||||||
|
if (this.props.canonicalAliasEvent) {
|
||||||
|
const altAliases = this.props.canonicalAliasEvent.getContent().alt_aliases;
|
||||||
|
if (Array.isArray(altAliases)) {
|
||||||
|
return altAliases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
changeCanonicalAlias(alias) {
|
changeCanonicalAlias(alias) {
|
||||||
if (!this.props.canSetCanonicalAlias) return;
|
if (!this.props.canSetCanonicalAlias) return;
|
||||||
|
|
||||||
|
@ -126,6 +170,8 @@ export default class AliasSettings extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventContent = {};
|
const eventContent = {};
|
||||||
|
const altAliases = this._getAltAliases();
|
||||||
|
if (altAliases) eventContent["alt_aliases"] = altAliases;
|
||||||
if (alias) eventContent["alias"] = alias;
|
if (alias) eventContent["alias"] = alias;
|
||||||
|
|
||||||
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
|
||||||
|
@ -261,26 +307,34 @@ export default class AliasSettings extends React.Component {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let localAliasesList;
|
||||||
|
if (this.state.localAliasesLoading) {
|
||||||
|
const Spinner = sdk.getComponent("elements.Spinner");
|
||||||
|
localAliasesList = <Spinner />;
|
||||||
|
} else {
|
||||||
|
localAliasesList = <EditableAliasesList
|
||||||
|
id="roomAliases"
|
||||||
|
className={"mx_RoomSettings_localAliases"}
|
||||||
|
items={this.state.domainToAliases[localDomain] || []}
|
||||||
|
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},
|
||||||
|
)}
|
||||||
|
domain={localDomain}
|
||||||
|
/>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='mx_AliasSettings'>
|
<div className='mx_AliasSettings'>
|
||||||
{canonicalAliasSection}
|
{canonicalAliasSection}
|
||||||
<EditableAliasesList
|
{localAliasesList}
|
||||||
id="roomAliases"
|
|
||||||
className={"mx_RoomSettings_localAliases"}
|
|
||||||
items={this.state.domainToAliases[localDomain] || []}
|
|
||||||
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},
|
|
||||||
)}
|
|
||||||
domain={localDomain}
|
|
||||||
/>
|
|
||||||
{remoteAliasesSection}
|
{remoteAliasesSection}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -219,7 +219,7 @@ export default createReactClass({
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
span = (
|
span = (
|
||||||
<a href={link} target="_blank" rel="noopener">
|
<a href={link} target="_blank" rel="noreferrer noopener">
|
||||||
{ span }
|
{ span }
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -392,6 +392,20 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
|
} else if (event.key === Key.ENTER && (event.shiftKey || (IS_MAC && event.altKey))) {
|
||||||
this._insertText("\n");
|
this._insertText("\n");
|
||||||
handled = true;
|
handled = true;
|
||||||
|
// move selection to start of composer
|
||||||
|
} else if (modKey && event.key === Key.HOME) {
|
||||||
|
setSelection(this._editorRef, model, {
|
||||||
|
index: 0,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
handled = true;
|
||||||
|
// move selection to end of composer
|
||||||
|
} else if (modKey && event.key === Key.END) {
|
||||||
|
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.
|
// autocomplete or enter to send below shouldn't have any modifier keys pressed.
|
||||||
} else {
|
} else {
|
||||||
const metaOrAltPressed = event.metaKey || event.altKey;
|
const metaOrAltPressed = event.metaKey || event.altKey;
|
||||||
|
@ -490,6 +504,7 @@ export default class BasicMessageEditor extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("selectionchange", this._onSelectionChange);
|
||||||
this._editorRef.removeEventListener("input", this._onInput, true);
|
this._editorRef.removeEventListener("input", this._onInput, true);
|
||||||
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
|
this._editorRef.removeEventListener("compositionstart", this._onCompositionStart, true);
|
||||||
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
|
this._editorRef.removeEventListener("compositionend", this._onCompositionEnd, true);
|
||||||
|
|
|
@ -51,7 +51,7 @@ const legacyRoomTitles = {
|
||||||
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
|
[E2E_STATE.VERIFIED]: _td("All sessions in this encrypted room are trusted"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const E2EIcon = ({isUser, status, className, size, onClick}) => {
|
const E2EIcon = ({isUser, status, className, size, onClick, hideTooltip}) => {
|
||||||
const [hover, setHover] = useState(false);
|
const [hover, setHover] = useState(false);
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
|
@ -82,7 +82,7 @@ const E2EIcon = ({isUser, status, className, size, onClick}) => {
|
||||||
const onMouseOut = () => setHover(false);
|
const onMouseOut = () => setHover(false);
|
||||||
|
|
||||||
let tip;
|
let tip;
|
||||||
if (hover) {
|
if (hover && !hideTooltip) {
|
||||||
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
tip = <Tooltip label={e2eTitle ? _t(e2eTitle) : ""} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -136,7 +136,7 @@ export default createReactClass({
|
||||||
<div className="mx_LinkPreviewWidget" >
|
<div className="mx_LinkPreviewWidget" >
|
||||||
{ img }
|
{ img }
|
||||||
<div className="mx_LinkPreviewWidget_caption">
|
<div className="mx_LinkPreviewWidget_caption">
|
||||||
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noopener">{ p["og:title"] }</a></div>
|
<div className="mx_LinkPreviewWidget_title"><a href={this.props.link} target="_blank" rel="noreferrer noopener">{ p["og:title"] }</a></div>
|
||||||
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
<div className="mx_LinkPreviewWidget_siteName">{ p["og:site_name"] ? (" - " + p["og:site_name"]) : null }</div>
|
||||||
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
|
<div className="mx_LinkPreviewWidget_description" ref={this._description}>
|
||||||
{ description }
|
{ description }
|
||||||
|
|
|
@ -341,7 +341,7 @@ export default class MessageComposer extends React.Component {
|
||||||
</a>
|
</a>
|
||||||
) : '';
|
) : '';
|
||||||
|
|
||||||
controls.push(<div className="mx_MessageComposer_replaced_wrapper">
|
controls.push(<div className="mx_MessageComposer_replaced_wrapper" key="room_replaced">
|
||||||
<div className="mx_MessageComposer_replaced_valign">
|
<div className="mx_MessageComposer_replaced_valign">
|
||||||
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
|
<img className="mx_MessageComposer_roomReplaced_icon" src={require("../../../../res/img/room_replaced.svg")} />
|
||||||
<span className="mx_MessageComposer_roomReplaced_header">
|
<span className="mx_MessageComposer_roomReplaced_header">
|
||||||
|
|
|
@ -314,7 +314,7 @@ export default createReactClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_RoomHeader light-panel">
|
<div className="mx_RoomHeader light-panel">
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper" aria-owns="mx_RightPanel">
|
||||||
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
|
<div className="mx_RoomHeader_avatar">{ roomAvatar }{ e2eIcon }</div>
|
||||||
{ privateIcon }
|
{ privateIcon }
|
||||||
{ name }
|
{ name }
|
||||||
|
|
|
@ -509,7 +509,7 @@ export default createReactClass({
|
||||||
"<issueLink>submit a bug report</issueLink>.",
|
"<issueLink>submit a bug report</issueLink>.",
|
||||||
{ errcode: this.props.error.errcode },
|
{ errcode: this.props.error.errcode },
|
||||||
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
{ issueLink: label => <a href="https://github.com/vector-im/riot-web/issues/new/choose"
|
||||||
target="_blank" rel="noopener">{ label }</a> },
|
target="_blank" rel="noreferrer noopener">{ label }</a> },
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -166,7 +166,9 @@ export default createReactClass({
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Check all verified user devices. */
|
/* Check all verified user devices. */
|
||||||
for (const userId of [...verified, cli.getUserId()]) {
|
/* Don't alarm if no other users are verified */
|
||||||
|
const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified;
|
||||||
|
for (const userId of targets) {
|
||||||
const devices = await cli.getStoredDevicesForUser(userId);
|
const devices = await cli.getStoredDevicesForUser(userId);
|
||||||
const allDevicesVerified = devices.every(({deviceId}) => {
|
const allDevicesVerified = devices.every(({deviceId}) => {
|
||||||
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
return cli.checkDeviceTrust(userId, deviceId).isVerified();
|
||||||
|
|
|
@ -213,7 +213,7 @@ export default createReactClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="mx_WhoIsTypingTile">
|
<li className="mx_WhoIsTypingTile" aria-atomic="true">
|
||||||
<div className="mx_WhoIsTypingTile_avatars">
|
<div className="mx_WhoIsTypingTile_avatars">
|
||||||
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
{ this._renderTypingIndicatorAvatars(usersTyping, this.props.whoIsTypingLimit) }
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -119,7 +119,7 @@ export default createReactClass({
|
||||||
'In future this will be improved.',
|
'In future this will be improved.',
|
||||||
) }
|
) }
|
||||||
{' '}
|
{' '}
|
||||||
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noopener">
|
<a href="https://github.com/vector-im/riot-web/issues/2671" target="_blank" rel="noreferrer noopener">
|
||||||
https://github.com/vector-im/riot-web/issues/2671
|
https://github.com/vector-im/riot-web/issues/2671
|
||||||
</a>
|
</a>
|
||||||
</div>,
|
</div>,
|
||||||
|
@ -253,20 +253,24 @@ export default createReactClass({
|
||||||
<form className={this.props.className} onSubmit={this.onClickChange}>
|
<form className={this.props.className} onSubmit={this.onClickChange}>
|
||||||
{ currentPassword }
|
{ currentPassword }
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
<Field id="mx_ChangePassword_newPassword"
|
<Field
|
||||||
|
id="mx_ChangePassword_newPassword"
|
||||||
type="password"
|
type="password"
|
||||||
label={passwordLabel}
|
label={passwordLabel}
|
||||||
value={this.state.newPassword}
|
value={this.state.newPassword}
|
||||||
autoFocus={this.props.autoFocusNewPasswordInput}
|
autoFocus={this.props.autoFocusNewPasswordInput}
|
||||||
onChange={this.onChangeNewPassword}
|
onChange={this.onChangeNewPassword}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={rowClassName}>
|
<div className={rowClassName}>
|
||||||
<Field id="mx_ChangePassword_newPasswordConfirm"
|
<Field
|
||||||
|
id="mx_ChangePassword_newPasswordConfirm"
|
||||||
type="password"
|
type="password"
|
||||||
label={_t("Confirm password")}
|
label={_t("Confirm password")}
|
||||||
value={this.state.newPasswordConfirm}
|
value={this.state.newPasswordConfirm}
|
||||||
onChange={this.onChangeNewPasswordConfirm}
|
onChange={this.onChangeNewPasswordConfirm}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
|
<AccessibleButton className={buttonClassName} kind={this.props.buttonKind} onClick={this.onClickChange}>
|
||||||
|
|
|
@ -20,6 +20,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import * as sdk from '../../../index';
|
import * as sdk from '../../../index';
|
||||||
import { accessSecretStorage } from '../../../CrossSigningManager';
|
import { accessSecretStorage } from '../../../CrossSigningManager';
|
||||||
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
export default class CrossSigningPanel extends React.PureComponent {
|
export default class CrossSigningPanel extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -86,11 +87,12 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
* 2. Access existing secret storage by requesting passphrase and accessing
|
* 2. Access existing secret storage by requesting passphrase and accessing
|
||||||
* cross-signing keys as needed.
|
* cross-signing keys as needed.
|
||||||
* 3. All keys are loaded and there's nothing to do.
|
* 3. All keys are loaded and there's nothing to do.
|
||||||
|
* @param {bool} [force] Bootstrap again even if keys already present
|
||||||
*/
|
*/
|
||||||
_bootstrapSecureSecretStorage = async () => {
|
_bootstrapSecureSecretStorage = async (force=false) => {
|
||||||
this.setState({ error: null });
|
this.setState({ error: null });
|
||||||
try {
|
try {
|
||||||
await accessSecretStorage();
|
await accessSecretStorage(() => undefined, force);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.setState({ error: e });
|
this.setState({ error: e });
|
||||||
console.error("Error bootstrapping secret storage", e);
|
console.error("Error bootstrapping secret storage", e);
|
||||||
|
@ -99,6 +101,18 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
this._getUpdatedStatus();
|
this._getUpdatedStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDestroyStorage = (act) => {
|
||||||
|
if (!act) return;
|
||||||
|
this._bootstrapSecureSecretStorage(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
_destroySecureSecretStorage = () => {
|
||||||
|
const ConfirmDestoryCrossSigningDialog = sdk.getComponent("dialogs.ConfirmDestroyCrossSigningDialog");
|
||||||
|
Modal.createDialog(ConfirmDestoryCrossSigningDialog, {
|
||||||
|
onFinished: this.onDestroyStorage,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
|
||||||
const {
|
const {
|
||||||
|
@ -114,13 +128,12 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
const enabled = (
|
const enabled = (
|
||||||
crossSigningPublicKeysOnDevice &&
|
|
||||||
crossSigningPrivateKeysInStorage &&
|
crossSigningPrivateKeysInStorage &&
|
||||||
secretStorageKeyInAccount
|
secretStorageKeyInAccount
|
||||||
);
|
);
|
||||||
|
|
||||||
let summarisedStatus;
|
let summarisedStatus;
|
||||||
if (enabled) {
|
if (enabled && crossSigningPublicKeysOnDevice) {
|
||||||
summarisedStatus = <p>✅ {_t(
|
summarisedStatus = <p>✅ {_t(
|
||||||
"Cross-signing and secret storage are enabled.",
|
"Cross-signing and secret storage are enabled.",
|
||||||
)}</p>;
|
)}</p>;
|
||||||
|
@ -142,6 +155,12 @@ export default class CrossSigningPanel extends React.PureComponent {
|
||||||
{_t("Bootstrap cross-signing and secret storage")}
|
{_t("Bootstrap cross-signing and secret storage")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>;
|
</div>;
|
||||||
|
} else {
|
||||||
|
bootstrapButton = <div className="mx_CrossSigningPanel_buttonRow">
|
||||||
|
<AccessibleButton kind="danger" onClick={this._destroySecureSecretStorage}>
|
||||||
|
{_t("Reset cross-signing and secret storage")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -37,21 +37,29 @@ export default class EventIndexPanel extends React.Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateCurrentRoom(room) {
|
updateCurrentRoom = async (room) => {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
const stats = await eventIndex.getStats();
|
let stats;
|
||||||
|
|
||||||
|
try {
|
||||||
|
stats = await eventIndex.getStats();
|
||||||
|
} catch {
|
||||||
|
// This call may fail if sporadically, not a huge issue as we will
|
||||||
|
// try later again and probably succeed.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
eventIndexSize: stats.size,
|
eventIndexSize: stats.size,
|
||||||
roomCount: stats.roomCount,
|
roomCount: stats.roomCount,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
const eventIndex = EventIndexPeg.get();
|
const eventIndex = EventIndexPeg.get();
|
||||||
|
|
||||||
if (eventIndex !== null) {
|
if (eventIndex !== null) {
|
||||||
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
eventIndex.removeListener("changedCheckpoint", this.updateCurrentRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,11 +76,17 @@ export default class EventIndexPanel extends React.Component {
|
||||||
let roomCount = 0;
|
let roomCount = 0;
|
||||||
|
|
||||||
if (eventIndex !== null) {
|
if (eventIndex !== null) {
|
||||||
eventIndex.on("changedCheckpoint", this.updateCurrentRoom.bind(this));
|
eventIndex.on("changedCheckpoint", this.updateCurrentRoom);
|
||||||
|
|
||||||
const stats = await eventIndex.getStats();
|
try {
|
||||||
eventIndexSize = stats.size;
|
const stats = await eventIndex.getStats();
|
||||||
roomCount = stats.roomCount;
|
eventIndexSize = stats.size;
|
||||||
|
roomCount = stats.roomCount;
|
||||||
|
} catch {
|
||||||
|
// This call may fail if sporadically, not a huge issue as we
|
||||||
|
// will try later again in the updateCurrentRoom call and
|
||||||
|
// probably succeed.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -158,7 +172,7 @@ export default class EventIndexPanel extends React.Component {
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
|
'nativeLink': (sub) => <a href={nativeLink} target="_blank"
|
||||||
rel="noopener">{sub}</a>,
|
rel="noreferrer noopener">{sub}</a>,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -174,7 +188,7 @@ export default class EventIndexPanel extends React.Component {
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
|
'riotLink': (sub) => <a href="https://riot.im/download/desktop"
|
||||||
target="_blank" rel="noopener">{sub}</a>,
|
target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -132,10 +132,10 @@ export default class ProfileSettings extends React.Component {
|
||||||
{_t(
|
{_t(
|
||||||
"<a>Upgrade</a> to your own domain", {},
|
"<a>Upgrade</a> to your own domain", {},
|
||||||
{
|
{
|
||||||
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='' />
|
<img src={require("../../../../res/img/external-link.svg")} width="11" height="10" alt='' />
|
||||||
</a>
|
</a>
|
||||||
</span>;
|
</span>;
|
||||||
|
|
|
@ -68,7 +68,7 @@ export default class BridgeSettingsTab extends React.Component {
|
||||||
{
|
{
|
||||||
// TODO: We don't have this link yet: this will prevent the translators
|
// TODO: We don't have this link yet: this will prevent the translators
|
||||||
// having to re-translate the string when we do.
|
// having to re-translate the string when we do.
|
||||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
|
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</p>
|
)}</p>
|
||||||
<ul className="mx_RoomSettingsDialog_BridgeList">
|
<ul className="mx_RoomSettingsDialog_BridgeList">
|
||||||
|
@ -82,7 +82,7 @@ export default class BridgeSettingsTab extends React.Component {
|
||||||
{
|
{
|
||||||
// TODO: We don't have this link yet: this will prevent the translators
|
// TODO: We don't have this link yet: this will prevent the translators
|
||||||
// having to re-translate the string when we do.
|
// having to re-translate the string when we do.
|
||||||
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noopener">{sub}</a>,
|
a: sub => <a href={BRIDGES_LINK} target="_blank" rel="noreferrer noopener">{sub}</a>,
|
||||||
},
|
},
|
||||||
)}</p>;
|
)}</p>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,20 +107,20 @@ export default class RolesRoomSettingsTab extends React.Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership.bind(this));
|
MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount(): void {
|
componentWillUnmount(): void {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
if (client) {
|
if (client) {
|
||||||
client.removeListener("RoomState.members", this._onRoomMembership.bind(this));
|
client.removeListener("RoomState.members", this._onRoomMembership);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomMembership(event, state, member) {
|
_onRoomMembership = (event, state, member) => {
|
||||||
if (state.roomId !== this.props.roomId) return;
|
if (state.roomId !== this.props.roomId) return;
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
};
|
||||||
|
|
||||||
_populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) {
|
_populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) {
|
||||||
for (const desiredEvent of Object.keys(plEventsToShow)) {
|
for (const desiredEvent of Object.keys(plEventsToShow)) {
|
||||||
|
|
|
@ -36,11 +36,12 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||||
joinRule: "invite",
|
joinRule: "invite",
|
||||||
guestAccess: "can_join",
|
guestAccess: "can_join",
|
||||||
history: "shared",
|
history: "shared",
|
||||||
|
hasAliases: false,
|
||||||
encrypted: false,
|
encrypted: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount(): void {
|
async componentWillMount(): void {
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
const room = MatrixClientPeg.get().getRoom(this.props.roomId);
|
||||||
|
@ -63,6 +64,8 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||||
);
|
);
|
||||||
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
const encrypted = MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
|
||||||
this.setState({joinRule, guestAccess, history, encrypted});
|
this.setState({joinRule, guestAccess, history, encrypted});
|
||||||
|
const hasAliases = await this._hasAliases();
|
||||||
|
this.setState({hasAliases});
|
||||||
}
|
}
|
||||||
|
|
||||||
_pullContentPropertyFromEvent(event, key, defaultValue) {
|
_pullContentPropertyFromEvent(event, key, defaultValue) {
|
||||||
|
@ -94,7 +97,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
'a': (sub) => {
|
'a': (sub) => {
|
||||||
return <a rel='noopener' target='_blank'
|
return <a rel='noreferrer noopener' target='_blank'
|
||||||
href='https://about.riot.im/help#end-to-end-encryption'>{sub}</a>;
|
href='https://about.riot.im/help#end-to-end-encryption'>{sub}</a>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -201,13 +204,25 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||||
MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked);
|
MatrixClientPeg.get().getRoom(this.props.roomId).setBlacklistUnverifiedDevices(checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async _hasAliases() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (await cli.doesServerSupportUnstableFeature("org.matrix.msc2432")) {
|
||||||
|
const response = await cli.unstableGetLocalAliases(this.props.roomId);
|
||||||
|
const localAliases = response.aliases;
|
||||||
|
return Array.isArray(localAliases) && localAliases.length !== 0;
|
||||||
|
} else {
|
||||||
|
const room = cli.getRoom(this.props.roomId);
|
||||||
|
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
|
||||||
|
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
|
||||||
|
return hasAliases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_renderRoomAccess() {
|
_renderRoomAccess() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
const joinRule = this.state.joinRule;
|
const joinRule = this.state.joinRule;
|
||||||
const guestAccess = this.state.guestAccess;
|
const guestAccess = this.state.guestAccess;
|
||||||
const aliasEvents = room.currentState.getStateEvents("m.room.aliases") || [];
|
|
||||||
const hasAliases = !!aliasEvents.find((ev) => (ev.getContent().aliases || []).length > 0);
|
|
||||||
|
|
||||||
const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
|
const canChangeAccess = room.currentState.mayClientSendStateEvent("m.room.join_rules", client)
|
||||||
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
|
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
|
||||||
|
@ -226,7 +241,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
let aliasWarning = null;
|
let aliasWarning = null;
|
||||||
if (joinRule === 'public' && !hasAliases) {
|
if (joinRule === 'public' && !this.state.hasAliases) {
|
||||||
aliasWarning = (
|
aliasWarning = (
|
||||||
<div className='mx_SecurityRoomSettingsTab_warning'>
|
<div className='mx_SecurityRoomSettingsTab_warning'>
|
||||||
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
<img src={require("../../../../../../res/img/warning.svg")} width={15} height={15} />
|
||||||
|
|
|
@ -37,7 +37,7 @@ const ghVersionLabel = function(repo, token='') {
|
||||||
} else {
|
} else {
|
||||||
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
url = `https://github.com/${repo}/commit/${token.split('-')[0]}`;
|
||||||
}
|
}
|
||||||
return <a target="_blank" rel="noopener" href={url}>{ token }</a>;
|
return <a target="_blank" rel="noreferrer noopener" href={url}>{ token }</a>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class HelpUserSettingsTab extends React.Component {
|
export default class HelpUserSettingsTab extends React.Component {
|
||||||
|
@ -110,7 +110,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||||
const legalLinks = [];
|
const legalLinks = [];
|
||||||
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
|
for (const tocEntry of SdkConfig.get().terms_and_conditions_links) {
|
||||||
legalLinks.push(<div key={tocEntry.url}>
|
legalLinks.push(<div key={tocEntry.url}>
|
||||||
<a href={tocEntry.url} rel="noopener" target="_blank">{tocEntry.text}</a>
|
<a href={tocEntry.url} rel="noreferrer noopener" target="_blank">{tocEntry.text}</a>
|
||||||
</div>);
|
</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,27 +132,27 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||||
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
|
<span className='mx_SettingsTab_subheading'>{_t("Credits")}</span>
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noopener" target="_blank">
|
The <a href="themes/riot/img/backgrounds/valley.jpg" rel="noreferrer noopener" target="_blank">
|
||||||
default cover photo</a> is ©
|
default cover photo</a> is ©
|
||||||
<a href="https://www.flickr.com/golan" rel="noopener" target="_blank">Jesús Roncero</a>{' '}
|
<a href="https://www.flickr.com/golan" rel="noreferrer noopener" target="_blank">Jesús Roncero</a>{' '}
|
||||||
used under the terms of
|
used under the terms of
|
||||||
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noopener" target="_blank">
|
<a href="https://creativecommons.org/licenses/by-sa/4.0/" rel="noreferrer noopener" target="_blank">
|
||||||
CC-BY-SA 4.0</a>.
|
CC-BY-SA 4.0</a>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noopener" target="_blank">
|
The <a href="https://github.com/matrix-org/twemoji-colr" rel="noreferrer noopener"
|
||||||
twemoji-colr</a> font is ©
|
target="_blank"> twemoji-colr</a> font is ©
|
||||||
<a href="https://mozilla.org" rel="noopener" target="_blank">Mozilla Foundation</a>{' '}
|
<a href="https://mozilla.org" rel="noreferrer noopener" target="_blank">Mozilla Foundation</a>{' '}
|
||||||
used under the terms of
|
used under the terms of
|
||||||
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noopener" target="_blank">
|
<a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">
|
||||||
Apache 2.0</a>.
|
Apache 2.0</a>.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
The <a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">
|
The <a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">
|
||||||
Twemoji</a> emoji art is ©
|
Twemoji</a> emoji art is ©
|
||||||
<a href="https://twemoji.twitter.com/" rel="noopener" target="_blank">Twitter, Inc and other
|
<a href="https://twemoji.twitter.com/" rel="noreferrer noopener" target="_blank">Twitter, Inc and other
|
||||||
contributors</a> used under the terms of
|
contributors</a> used under the terms of
|
||||||
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noopener" target="_blank">
|
<a href="https://creativecommons.org/licenses/by/4.0/" rel="noreferrer noopener" target="_blank">
|
||||||
CC-BY 4.0</a>.
|
CC-BY 4.0</a>.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -162,7 +162,8 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
|
let faqText = _t('For help with using Riot, click <a>here</a>.', {}, {
|
||||||
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener' target='_blank'>{sub}</a>,
|
'a': (sub) =>
|
||||||
|
<a href="https://about.riot.im/need-help/" rel='noreferrer noopener' target='_blank'>{sub}</a>,
|
||||||
});
|
});
|
||||||
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
|
if (SdkConfig.get().welcomeUserId && getCurrentLanguage().startsWith('en')) {
|
||||||
faqText = (
|
faqText = (
|
||||||
|
@ -170,7 +171,7 @@ export default class HelpUserSettingsTab extends React.Component {
|
||||||
{
|
{
|
||||||
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
|
_t('For help with using Riot, click <a>here</a> or start a chat with our ' +
|
||||||
'bot using the button below.', {}, {
|
'bot using the button below.', {}, {
|
||||||
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noopener'
|
'a': (sub) => <a href="https://about.riot.im/need-help/" rel='noreferrer noopener'
|
||||||
target='_blank'>{sub}</a>,
|
target='_blank'>{sub}</a>,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,7 +55,7 @@ export default class LabsUserSettingsTab extends React.Component {
|
||||||
'<a>Learn more</a>.', {}, {
|
'<a>Learn more</a>.', {}, {
|
||||||
'a': (sub) => {
|
'a': (sub) => {
|
||||||
return <a href="https://github.com/vector-im/riot-web/blob/develop/docs/labs.md"
|
return <a href="https://github.com/vector-im/riot-web/blob/develop/docs/labs.md"
|
||||||
rel='noopener' target='_blank'>{sub}</a>;
|
rel='noreferrer noopener' target='_blank'>{sub}</a>;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
|
||||||
];
|
];
|
||||||
|
|
||||||
static TIMELINE_SETTINGS = [
|
static TIMELINE_SETTINGS = [
|
||||||
|
'showTypingNotifications',
|
||||||
'autoplayGifsAndVideos',
|
'autoplayGifsAndVideos',
|
||||||
'urlPreviewsEnabled',
|
'urlPreviewsEnabled',
|
||||||
'TextualBody.enableBigEmoji',
|
'TextualBody.enableBigEmoji',
|
||||||
|
|
|
@ -77,7 +77,7 @@ export default class InlineTermsAgreement extends React.Component {
|
||||||
"Accept <policyLink /> to continue:", {}, {
|
"Accept <policyLink /> to continue:", {}, {
|
||||||
policyLink: () => {
|
policyLink: () => {
|
||||||
return (
|
return (
|
||||||
<a href={policy.url} rel='noopener' target='_blank'>
|
<a href={policy.url} rel='noreferrer noopener' target='_blank'>
|
||||||
{policy.name}
|
{policy.name}
|
||||||
<span className='mx_InlineTermsAgreement_link' />
|
<span className='mx_InlineTermsAgreement_link' />
|
||||||
</a>
|
</a>
|
||||||
|
|
|
@ -58,10 +58,7 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||||
|
|
||||||
_checkRequestIsPending = () => {
|
_checkRequestIsPending = () => {
|
||||||
const {request} = this.props;
|
const {request} = this.props;
|
||||||
const isPendingInRoomRequest = request.channel.roomId &&
|
if (!request.canAccept) {
|
||||||
!(request.ready || request.started || request.done || request.cancelled || request.observeOnly);
|
|
||||||
const isPendingDeviceRequest = request.channel.deviceId && request.started;
|
|
||||||
if (!isPendingInRoomRequest && !isPendingDeviceRequest) {
|
|
||||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -79,15 +76,15 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||||
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
|
||||||
const {request} = this.props;
|
const {request} = this.props;
|
||||||
// no room id for to_device requests
|
// no room id for to_device requests
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
try {
|
try {
|
||||||
|
await request.accept();
|
||||||
if (request.channel.roomId) {
|
if (request.channel.roomId) {
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: request.channel.roomId,
|
room_id: request.channel.roomId,
|
||||||
should_peek: false,
|
should_peek: false,
|
||||||
});
|
});
|
||||||
await request.accept();
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "set_right_panel_phase",
|
action: "set_right_panel_phase",
|
||||||
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
|
||||||
|
@ -96,11 +93,10 @@ export default class VerificationRequestToast extends React.PureComponent {
|
||||||
member: cli.getUser(request.otherUserId),
|
member: cli.getUser(request.otherUserId),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else if (request.channel.deviceId && request.verifier) {
|
} else {
|
||||||
// show to_device verifications in dialog still
|
const VerificationRequestDialog = sdk.getComponent("views.dialogs.VerificationRequestDialog");
|
||||||
const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog");
|
Modal.createTrackedDialog('Incoming Verification', '', VerificationRequestDialog, {
|
||||||
Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, {
|
verificationRequest: request,
|
||||||
verifier: request.verifier,
|
|
||||||
}, null, /* priority = */ false, /* static = */ true);
|
}, null, /* priority = */ false, /* static = */ true);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
/*
|
||||||
|
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 { _t } from '../../../languageHandler';
|
||||||
|
import AccessibleButton from "../elements/AccessibleButton";
|
||||||
|
import {replaceableComponent} from "../../../utils/replaceableComponent";
|
||||||
|
import VerificationQRCode from "../elements/crypto/VerificationQRCode";
|
||||||
|
import {VerificationRequest} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
|
||||||
|
import Spinner from "../elements/Spinner";
|
||||||
|
|
||||||
|
@replaceableComponent("views.verification.VerificationQREmojiOptions")
|
||||||
|
export default class VerificationQREmojiOptions extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
request: PropTypes.object.isRequired,
|
||||||
|
onCancel: PropTypes.func.isRequired,
|
||||||
|
onStartEmoji: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
qrProps: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._prepareQrCode(props.request);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _prepareQrCode(request: VerificationRequest) {
|
||||||
|
try {
|
||||||
|
const props = await VerificationQRCode.getPropsForRequest(request);
|
||||||
|
this.setState({qrProps: props});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
// We just won't show a QR code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let qrCode = <div className='mx_VerificationQREmojiOptions_noQR'><Spinner /></div>;
|
||||||
|
if (this.state.qrProps) {
|
||||||
|
qrCode = <VerificationQRCode {...this.state.qrProps} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{_t("Verify this session by completing one of the following:")}
|
||||||
|
<div className='mx_IncomingSasDialog_startOptions'>
|
||||||
|
<div className='mx_IncomingSasDialog_startOption'>
|
||||||
|
<p>{_t("Scan this unique code")}</p>
|
||||||
|
{qrCode}
|
||||||
|
</div>
|
||||||
|
<div className='mx_IncomingSasDialog_betweenText'>{_t("or")}</div>
|
||||||
|
<div className='mx_IncomingSasDialog_startOption'>
|
||||||
|
<p>{_t("Compare unique emoji")}</p>
|
||||||
|
<span className='mx_IncomingSasDialog_helpText'>{_t("Compare a unique set of emoji if you don't have a camera on either device")}</span>
|
||||||
|
<AccessibleButton onClick={this.props.onStartEmoji} kind='primary'>
|
||||||
|
{_t("Start")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<AccessibleButton onClick={this.props.onCancel} kind='danger'>
|
||||||
|
{_t("Cancel")}
|
||||||
|
</AccessibleButton>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ import dis from "./dispatcher";
|
||||||
import * as Rooms from "./Rooms";
|
import * as Rooms from "./Rooms";
|
||||||
import DMRoomMap from "./utils/DMRoomMap";
|
import DMRoomMap from "./utils/DMRoomMap";
|
||||||
import {getAddressType} from "./UserAddress";
|
import {getAddressType} from "./UserAddress";
|
||||||
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new room, and switch to it.
|
* Create a new room, and switch to it.
|
||||||
|
@ -159,7 +160,7 @@ export default function createRoom(opts) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureDMExists(client, userId) {
|
export function findDMForUser(client, userId) {
|
||||||
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
const roomIds = DMRoomMap.shared().getDMRoomsForUserId(userId);
|
||||||
const rooms = roomIds.map(id => client.getRoom(id));
|
const rooms = roomIds.map(id => client.getRoom(id));
|
||||||
const suitableDMRooms = rooms.filter(r => {
|
const suitableDMRooms = rooms.filter(r => {
|
||||||
|
@ -169,12 +170,60 @@ export async function ensureDMExists(client, userId) {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
let roomId;
|
|
||||||
if (suitableDMRooms.length) {
|
if (suitableDMRooms.length) {
|
||||||
const room = suitableDMRooms[0];
|
return suitableDMRooms[0];
|
||||||
roomId = room.roomId;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Try to ensure the user is already in the megolm session before continuing
|
||||||
|
* NOTE: this assumes you've just created the room and there's not been an opportunity
|
||||||
|
* for other code to run, so we shouldn't miss RoomState.newMember when it comes by.
|
||||||
|
*/
|
||||||
|
export async function _waitForMember(client, roomId, userId, opts = { timeout: 1500 }) {
|
||||||
|
const { timeout } = opts;
|
||||||
|
let handler;
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
handler = function(_event, _roomstate, member) {
|
||||||
|
if (member.userId !== userId) return;
|
||||||
|
if (member.roomId !== roomId) return;
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
client.on("RoomState.newMember", handler);
|
||||||
|
|
||||||
|
/* We don't want to hang if this goes wrong, so we proceed and hope the other
|
||||||
|
user is already in the megolm session */
|
||||||
|
setTimeout(resolve, timeout, false);
|
||||||
|
}).finally(() => {
|
||||||
|
client.removeListener("RoomState.newMember", handler);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ensure that for every user in a room, there is at least one device that we
|
||||||
|
* can encrypt to.
|
||||||
|
*/
|
||||||
|
export async function canEncryptToAllUsers(client, userIds) {
|
||||||
|
const usersDeviceMap = await client.downloadKeys(userIds);
|
||||||
|
// { "@user:host": { "DEVICE": {...}, ... }, ... }
|
||||||
|
return Object.values(usersDeviceMap).every((userDevices) =>
|
||||||
|
// { "DEVICE": {...}, ... }
|
||||||
|
Object.keys(userDevices).length > 0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureDMExists(client, userId) {
|
||||||
|
const existingDMRoom = findDMForUser(client, userId);
|
||||||
|
let roomId;
|
||||||
|
if (existingDMRoom) {
|
||||||
|
roomId = existingDMRoom.roomId;
|
||||||
} else {
|
} else {
|
||||||
roomId = await createRoom({dmUserId: userId, spinner: false, andView: false});
|
let encryption;
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_cross_signing")) {
|
||||||
|
encryption = canEncryptToAllUsers(client, [userId]);
|
||||||
|
}
|
||||||
|
roomId = await createRoom({encryption, dmUserId: userId, spinner: false, andView: false});
|
||||||
|
await _waitForMember(client, roomId, userId);
|
||||||
}
|
}
|
||||||
return roomId;
|
return roomId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ export default class AutocompleteWrapperModel {
|
||||||
const text = completion.completion;
|
const text = completion.completion;
|
||||||
switch (completion.type) {
|
switch (completion.type) {
|
||||||
case "room":
|
case "room":
|
||||||
return [this._partCreator.roomPill(completionId), this._partCreator.plain(completion.suffix)];
|
return [this._partCreator.roomPill(text, completionId), this._partCreator.plain(completion.suffix)];
|
||||||
case "at-room":
|
case "at-room":
|
||||||
return [this._partCreator.atRoomPill(completionId), this._partCreator.plain(completion.suffix)];
|
return [this._partCreator.atRoomPill(completionId), this._partCreator.plain(completion.suffix)];
|
||||||
case "user":
|
case "user":
|
||||||
|
|
|
@ -254,8 +254,8 @@ class RoomPillPart extends PillPart {
|
||||||
let initialLetter = "";
|
let initialLetter = "";
|
||||||
let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio);
|
let avatarUrl = Avatar.avatarUrlForRoom(this._room, 16 * window.devicePixelRatio, 16 * window.devicePixelRatio);
|
||||||
if (!avatarUrl) {
|
if (!avatarUrl) {
|
||||||
initialLetter = Avatar.getInitialLetter(this._room.name);
|
initialLetter = Avatar.getInitialLetter(this._room ? this._room.name : this.resourceId);
|
||||||
avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room.roomId)}`;
|
avatarUrl = `../../${Avatar.defaultAvatarUrlForString(this._room ? this._room.roomId : this.resourceId)}`;
|
||||||
}
|
}
|
||||||
this._setAvatarVars(node, avatarUrl, initialLetter);
|
this._setAvatarVars(node, avatarUrl, initialLetter);
|
||||||
}
|
}
|
||||||
|
@ -422,14 +422,15 @@ export class PartCreator {
|
||||||
return new PillCandidatePart(text, this._autoCompleteCreator);
|
return new PillCandidatePart(text, this._autoCompleteCreator);
|
||||||
}
|
}
|
||||||
|
|
||||||
roomPill(alias) {
|
roomPill(alias, roomId) {
|
||||||
let room;
|
let room;
|
||||||
if (alias[0] === '#') {
|
if (roomId || alias[0] !== "#") {
|
||||||
room = this._client.getRooms().find((r) => {
|
room = this._client.getRoom(roomId || alias);
|
||||||
return r.getCanonicalAlias() === alias || r.getAliases().includes(alias);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
room = this._client.getRoom(alias);
|
room = this._client.getRooms().find((r) => {
|
||||||
|
return r.getCanonicalAlias() === alias ||
|
||||||
|
r.getAltAliases().includes(alias);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return new RoomPillPart(alias, room);
|
return new RoomPillPart(alias, room);
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue