diff --git a/CHANGELOG.md b/CHANGELOG.md
index 881669a1a2..d4e0b41883 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,379 @@
+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 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)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.0.0-rc.2...v2.0.0)
diff --git a/docs/usercontent.md b/docs/usercontent.md
new file mode 100644
index 0000000000..e54851dd0d
--- /dev/null
+++ b/docs/usercontent.md
@@ -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 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.
diff --git a/package.json b/package.json
index 2c4d0144d4..793692d8b4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "2.0.0",
+ "version": "2.1.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
diff --git a/release.sh b/release.sh
index 1f287bc839..c585708326 100755
--- a/release.sh
+++ b/release.sh
@@ -9,4 +9,51 @@ set -e
cd `dirname $0`
+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
+
exec ./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
diff --git a/src/Analytics.js b/src/Analytics.js
index 39731a0e7d..8eea47ea89 100644
--- a/src/Analytics.js
+++ b/src/Analytics.js
@@ -57,6 +57,8 @@ function getRedactedUrl() {
}
const customVariables = {
+ // The Matomo installation at https://matomo.riot.im is currently configured
+ // with a limit of 10 custom variables.
'App Platform': {
id: 1,
expl: _td('The platform you\'re on'),
@@ -64,7 +66,7 @@ const customVariables = {
},
'App Version': {
id: 2,
- expl: _td('The version of Riot.im'),
+ expl: _td('The version of Riot'),
example: '15.0.0',
},
'User Type': {
@@ -87,20 +89,25 @@ const customVariables = {
expl: _td('Whether or not you\'re using the Richtext mode of the Rich Text Editor'),
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': {
id: 7,
expl: _td('Your homeserver\'s URL'),
example: 'https://matrix.org',
},
- 'Identity Server URL': {
+ 'Touch Input': {
id: 8,
- expl: _td('Your identity server\'s URL'),
- example: 'https://vector.im',
+ expl: _td("Whether you're using Riot on a device where touch is the primary input mechanism"),
+ 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',
},
};
@@ -190,6 +197,20 @@ class Analytics {
this._setVisitVariable('Instance', window.location.pathname);
}
+ let installedPWA = "unknown";
+ try {
+ // Known to work at least for desktop Chrome
+ installedPWA = window.matchMedia('(display-mode: standalone)').matches;
+ } catch (e) { }
+ this._setVisitVariable('Installed PWA', installedPWA);
+
+ let touchInput = "unknown";
+ try {
+ // MDN claims broad support across browsers
+ touchInput = window.matchMedia('(pointer: coarse)').matches;
+ } catch (e) { }
+ this._setVisitVariable('Touch Input', touchInput);
+
// start heartbeat
this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
}
@@ -291,11 +312,9 @@ class Analytics {
if (!config.piwik) return;
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
- const whitelistedISUrls = config.piwik.whitelistedISUrls || [];
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
- this._setVisitVariable('Identity Server URL', whitelistRedact(whitelistedISUrls, identityServerUrl));
}
setBreadcrumbs(state) {
@@ -328,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 },
];
@@ -337,7 +356,7 @@ class Analytics {
title: _t('Analytics'),
description:
- { _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:') }
{ rows.map((row) =>
diff --git a/src/Lifecycle.js b/src/Lifecycle.js
index 24bf99ed24..72cd84bfd9 100644
--- a/src/Lifecycle.js
+++ b/src/Lifecycle.js
@@ -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) {
try {
diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js
index 821e370628..b8b11fbb31 100644
--- a/src/components/structures/MessagePanel.js
+++ b/src/components/structures/MessagePanel.js
@@ -413,10 +413,6 @@ export default class MessagePanel extends React.Component {
};
_getEventTiles() {
- const DateSeparator = sdk.getComponent('messages.DateSeparator');
- const EventListSummary = sdk.getComponent('views.elements.EventListSummary');
- const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary');
-
this.eventNodes = {};
let i;
@@ -458,199 +454,48 @@ export default class MessagePanel extends React.Component {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
}
+ let grouper = null;
+
for (i = 0; i < this.props.events.length; i++) {
const mxEv = this.props.events[i];
const eventId = mxEv.getId();
const last = (mxEv === lastShownEvent);
- // 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
- const shouldGroup = (ev) => {
- if (ev.getType() === "m.room.member"
- && (ev.getStateKey() !== mxEv.getSender() || ev.getContent()["membership"] !== "join")) {
- return false;
- }
- if (ev.isState() && ev.getSender() === mxEv.getSender()) {
- return true;
- }
- 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 = ;
- ret.push(dateSeparator);
+ if (grouper) {
+ if (grouper.shouldGroup(mxEv)) {
+ grouper.add(mxEv);
+ continue;
+ } else {
+ // not part of group, so get the group tiles, close the
+ // group, and continue like a normal event
+ ret.push(...grouper.getTiles());
+ prevEvent = grouper.getNewPrevEvent();
+ grouper = null;
}
-
- // 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(
- { eventTiles }
- );
-
- if (summaryReadMarker) {
- ret.push(summaryReadMarker);
- }
-
- prevEvent = mxEv;
- continue;
}
- const wantTile = this._shouldShowEvent(mxEv);
-
- // Wrap consecutive member events in a ListSummary, ignore if redacted
- 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 = ;
- ret.push(dateSeparator);
+ for (const Grouper of groupers) {
+ if (Grouper.canStartGroup(this, mxEv)) {
+ grouper = new Grouper(this, mxEv, prevEvent, lastShownEvent);
}
-
- // 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(
- { eventTiles }
- );
-
- 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) {
- // 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;
+ const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
+ if (readMarker) ret.push(readMarker);
}
+ }
- const readMarker = this._readMarkerForEvent(eventId, i >= lastShownNonLocalEchoIndex);
- if (readMarker) ret.push(readMarker);
+ if (grouper) {
+ ret.push(...grouper.getTiles());
}
return ret;
@@ -961,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(
+ ,
+ );
+ }
+
+ // 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(
+
+ { eventTiles }
+ ,
+ );
+
+ 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(
+ ,
+ );
+ }
+
+ // 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(
+
+ { eventTiles }
+ ,
+ );
+
+ 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];
diff --git a/src/components/views/dialogs/InviteDialog.js b/src/components/views/dialogs/InviteDialog.js
index 8fec2437f6..8c9c5f75ef 100644
--- a/src/components/views/dialogs/InviteDialog.js
+++ b/src/components/views/dialogs/InviteDialog.js
@@ -31,7 +31,7 @@ import dis from "../../../dispatcher";
import IdentityAuthClient from "../../../IdentityAuthClient";
import Modal from "../../../Modal";
import {humanizeTime} from "../../../utils/humanize";
-import createRoom from "../../../createRoom";
+import createRoom, {canEncryptToAllUsers} from "../../../createRoom";
import {inviteMultipleToRoom} from "../../../RoomInvite";
import SettingsStore from '../../../settings/SettingsStore';
@@ -535,11 +535,7 @@ export default class InviteDialog extends React.PureComponent {
// Check whether all users have uploaded device keys before.
// If so, enable encryption in the new room.
const client = MatrixClientPeg.get();
- const usersToDevicesMap = await client.downloadKeys(targetIds);
- const allHaveDeviceKeys = Object.values(usersToDevicesMap).every(devices => {
- // `devices` is an object of the form { deviceId: deviceInfo, ... }.
- return Object.keys(devices).length > 0;
- });
+ const allHaveDeviceKeys = await canEncryptToAllUsers(client, targetIds);
if (allHaveDeviceKeys) {
createRoomOptions.encryption = true;
}
diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js
index 737c229afe..f67cd1b2b0 100644
--- a/src/components/views/messages/MFileBody.js
+++ b/src/components/views/messages/MFileBody.js
@@ -26,7 +26,7 @@ import {decryptFile} from '../../../utils/DecryptFile';
import Tinter from '../../../Tinter';
import request from 'browser-request';
import Modal from '../../../Modal';
-import SdkConfig from "../../../SdkConfig";
+import AccessibleButton from "../elements/AccessibleButton";
// 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 sandboxed iframe is that the browers are overly
// 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:
-//
-//
-//
-// 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.
@@ -283,7 +205,6 @@ export default createReactClass({
// will be inside the iframe so we wont be able to update
// it directly.
this._iframe.current.contentWindow.postMessage({
- code: remoteSetTint.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this._dummyLink.current),
}, "*");
@@ -306,7 +227,7 @@ export default createReactClass({
// Wait for the user to click on the link before downloading
// and decrypting the attachment.
let decrypting = false;
- const decrypt = () => {
+ const decrypt = (e) => {
if (decrypting) {
return false;
}
@@ -323,16 +244,15 @@ export default createReactClass({
});
}).finally(() => {
decrypting = false;
- return;
});
};
return (
);
@@ -341,7 +261,6 @@ export default createReactClass({
// When the iframe loads we tell it to render a download link
const onIframeLoad = (ev) => {
ev.target.contentWindow.postMessage({
- code: remoteRender.toString(),
imgSrc: tintedDownloadImageURL,
style: computedStyle(this._dummyLink.current),
blob: this.state.decryptedBlob,
@@ -349,19 +268,13 @@ export default createReactClass({
// 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.
download: fileName,
- rel: "noopener",
- target: "_blank",
textContent: _t("Download %(text)s", { text: text }),
}, "*");
};
- // If the attachment is encryped then put the link inside an iframe.
- let renderer_url = DEFAULT_CROSS_ORIGIN_RENDERER;
- const appConfig = SdkConfig.get();
- if (appConfig && appConfig.cross_origin_renderer_url) {
- renderer_url = appConfig.cross_origin_renderer_url;
- }
- renderer_url += "?origin=" + encodeURIComponent(window.location.origin);
+ const url = "usercontent/"; // XXX: this path should probably be passed from the skin
+
+ // If the attachment is encrypted then put the link inside an iframe.
return (
@@ -373,7 +286,11 @@ export default createReactClass({
*/ }
-
+
);
diff --git a/src/components/views/messages/MKeyVerificationRequest.js b/src/components/views/messages/MKeyVerificationRequest.js
index 9a48858bc7..d02319119e 100644
--- a/src/components/views/messages/MKeyVerificationRequest.js
+++ b/src/components/views/messages/MKeyVerificationRequest.js
@@ -59,7 +59,6 @@ export default class MKeyVerificationRequest extends React.Component {
};
_onAcceptClicked = async () => {
- this.setState({acceptOrCancelClicked: true});
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@@ -72,7 +71,6 @@ export default class MKeyVerificationRequest extends React.Component {
};
_onRejectClicked = async () => {
- this.setState({acceptOrCancelClicked: true});
const request = this.props.mxEvent.verificationRequest;
if (request) {
try {
@@ -96,10 +94,20 @@ export default class MKeyVerificationRequest extends React.Component {
_cancelledLabel(userId) {
const client = MatrixClientPeg.get();
const myUserId = client.getUserId();
+ const {cancellationCode} = this.props.mxEvent.verificationRequest;
+ const declined = cancellationCode === "m.user";
if (userId === myUserId) {
- return _t("You cancelled");
+ if (declined) {
+ return _t("You declined");
+ } else {
+ return _t("You cancelled");
+ }
} 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())});
+ }
}
}
@@ -118,15 +126,19 @@ export default class MKeyVerificationRequest extends React.Component {
let subtitle;
let stateNode;
- const accepted = request.ready || request.started || request.done;
- if (accepted || request.cancelled) {
+ if (!request.canAccept) {
let stateLabel;
+ const accepted = request.ready || request.started || request.done;
if (accepted) {
stateLabel = (
{this._acceptedLabel(request.receivingUserId)}
);
- } else {
+ } else if (request.cancelled) {
stateLabel = this._cancelledLabel(request.cancellingUserId);
+ } else if (request.accepting) {
+ stateLabel = _t("accepting …");
+ } else if (request.declining) {
+ stateLabel = _t("declining …");
}
stateNode = ({stateLabel}
);
}
@@ -137,11 +149,10 @@ export default class MKeyVerificationRequest extends React.Component {
_t("%(name)s wants to verify", {name})});
subtitle = ({
userLabelForEventRoom(request.requestingUserId, mxEvent.getRoomId())}
);
- if (request.requested && !request.observeOnly) {
- const disabled = this.state.acceptOrCancelClicked;
+ if (request.canAccept) {
stateNode = (
-
-
+
+
);
}
} else { // request sent by us
diff --git a/src/components/views/right_panel/EncryptionPanel.js b/src/components/views/right_panel/EncryptionPanel.js
index 2e9365fca3..a5685e0617 100644
--- a/src/components/views/right_panel/EncryptionPanel.js
+++ b/src/components/views/right_panel/EncryptionPanel.js
@@ -23,7 +23,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {ensureDMExists} from "../../../createRoom";
import {useEventEmitter} from "../../../hooks/useEventEmitter";
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 {_t} from "../../../languageHandler";
@@ -69,9 +69,10 @@ const EncryptionPanel = ({verificationRequest, member, onClose, layout}) => {
const roomId = await ensureDMExists(cli, member.userId);
const verificationRequest = await cli.requestVerificationDM(member.userId, roomId);
setRequest(verificationRequest);
+ setPhase(verificationRequest.phase);
}, [member.userId]);
- const requested = request && (phase === PHASE_REQUESTED || phase === undefined);
+ const requested = request && (phase === PHASE_REQUESTED || phase === PHASE_UNSENT || phase === undefined);
if (!request || requested) {
return ;
} else {
diff --git a/src/components/views/right_panel/UserInfo.js b/src/components/views/right_panel/UserInfo.js
index 315035db96..1174f45640 100644
--- a/src/components/views/right_panel/UserInfo.js
+++ b/src/components/views/right_panel/UserInfo.js
@@ -25,7 +25,7 @@ import dis from '../../../dispatcher';
import Modal from '../../../Modal';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
-import createRoom from '../../../createRoom';
+import createRoom, {findDMForUser} from '../../../createRoom';
import DMRoomMap from '../../../utils/DMRoomMap';
import AccessibleButton from '../elements/AccessibleButton';
import SdkConfig from '../../../SdkConfig';
@@ -169,10 +169,19 @@ async function verifyDevice(userId, device) {
}
function verifyUser(user) {
+ const cli = MatrixClientPeg.get();
+ const dmRoom = findDMForUser(cli, user.userId);
+ let existingRequest;
+ if (dmRoom) {
+ existingRequest = cli.findVerificationRequestDMInProgress(dmRoom.roomId);
+ }
dis.dispatch({
action: "set_right_panel_phase",
phase: RIGHT_PANEL_PHASES.EncryptionPanel,
- refireParams: {member: user},
+ refireParams: {
+ member: user,
+ verificationRequest: existingRequest,
+ },
});
}
diff --git a/src/components/views/right_panel/VerificationPanel.js b/src/components/views/right_panel/VerificationPanel.js
index 08c3935a2c..45a9b9eddb 100644
--- a/src/components/views/right_panel/VerificationPanel.js
+++ b/src/components/views/right_panel/VerificationPanel.js
@@ -19,6 +19,8 @@ import PropTypes from "prop-types";
import * as sdk from '../../../index';
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 {_t} from "../../../languageHandler";
import E2EIcon from "../rooms/E2EIcon";
@@ -54,7 +56,9 @@ export default class VerificationPanel extends React.PureComponent {
qrCodeProps: null, // generated by the VerificationQRCode component itself
};
this._hasVerifier = false;
- this._generateQRCodeProps(props.request);
+ if (this.props.request.otherPartySupportsMethod(SCAN_QR_CODE_METHOD)) {
+ this._generateQRCodeProps(props.request);
+ }
}
async _generateQRCodeProps(verificationRequest: VerificationRequest) {
@@ -67,59 +71,60 @@ export default class VerificationPanel extends React.PureComponent {
}
renderQRPhase(pending) {
- const {member} = 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 noCommonMethodError = !showSAS && !showQR ?
+ {_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.")}
:
+ null;
+
if (this.props.layout === 'dialog') {
// HACK: This is a terrible idea.
- let qrCode =
;
- if (this.state.qrCodeProps) {
- qrCode = ;
+ let qrBlock;
+ let sasBlock;
+ if (showQR) {
+ let qrCode;
+ if (this.state.qrCodeProps) {
+ qrCode = ;
+ } else {
+ qrCode =
;
+ }
+ qrBlock =
+
+
{_t("Scan this unique code")}
+ {qrCode}
+
;
}
+ if (showSAS) {
+ sasBlock =
+
+
{_t("Compare unique emoji")}
+
{_t("Compare a unique set of emoji if you don't have a camera on either device")}
+
+ {_t("Start")}
+
+
;
+ }
+ const or = qrBlock && sasBlock ?
+ {_t("or")}
: null;
return (
{_t("Verify this session by completing one of the following:")}
-
-
{_t("Scan this unique code")}
- {qrCode}
-
-
{_t("or")}
-
-
{_t("Compare unique emoji")}
-
{_t("Compare a unique set of emoji if you don't have a camera on either device")}
-
- {_t("Start")}
-
-
+ {qrBlock}
+ {or}
+ {sasBlock}
+ {noCommonMethodError}
);
}
- let button;
- if (pending) {
- button = ;
- } else {
- const disabled = this.state.emojiButtonClicked;
- button = (
-
- {_t("Verify by emoji")}
-
- );
- }
-
- if (!this.state.qrCodeProps) {
- return
-
{_t("Verify by emoji")}
-
{_t("Verify by comparing unique emoji.")}
- { button }
-
;
- }
-
- // TODO: add way to open camera to scan a QR code
- return
-
+ let qrBlock;
+ if (this.state.qrCodeProps) {
+ qrBlock =
{_t("Verify by scanning")}
{_t("Ask %(displayName)s to scan your code:", {
displayName: member.displayName || member.name || member.userId,
@@ -128,14 +133,41 @@ export default class VerificationPanel extends React.PureComponent {
-
+
;
+ }
-
+ let sasBlock;
+ if (showSAS) {
+ let button;
+ if (pending) {
+ button =
;
+ } else {
+ const disabled = this.state.emojiButtonClicked;
+ button = (
+
+ {_t("Verify by emoji")}
+
+ );
+ }
+ 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 =
{_t("Verify by emoji")}
-
{_t("If you can't scan the code above, verify by comparing unique emoji.")}
-
+
{sasLabel}
{ button }
-
+
;
+ }
+
+ const noCommonMethodBlock = noCommonMethodError ?
+ {noCommonMethodError}
:
+ null;
+
+ // TODO: add way to open camera to scan a QR code
+ return
+ {qrBlock}
+ {sasBlock}
+ {noCommonMethodBlock}
;
}
@@ -258,7 +290,11 @@ export default class VerificationPanel extends React.PureComponent {
};
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();
}
diff --git a/src/components/views/room_settings/AliasSettings.js b/src/components/views/room_settings/AliasSettings.js
index ee5a505b32..d7befa488d 100644
--- a/src/components/views/room_settings/AliasSettings.js
+++ b/src/components/views/room_settings/AliasSettings.js
@@ -74,7 +74,6 @@ export default class AliasSettings extends React.Component {
roomId: PropTypes.string.isRequired,
canSetCanonicalAlias: PropTypes.bool.isRequired,
canSetAliases: PropTypes.bool.isRequired,
- aliasEvents: PropTypes.array, // [MatrixEvent]
canonicalAliasEvent: PropTypes.object, // MatrixEvent
};
@@ -94,12 +93,6 @@ export default class AliasSettings extends React.Component {
updatingCanonicalAlias: false,
};
- 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) {
state.canonicalAlias = props.canonicalAliasEvent.getContent().alias;
}
@@ -107,6 +100,42 @@ export default class AliasSettings extends React.Component {
this.state = state;
}
+ async componentWillMount() {
+ const cli = MatrixClientPeg.get();
+ 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);
+ }
+ }
+
+ aliasesToDictionary(aliases) {
+ return aliases.reduce((dict, alias) => {
+ const domain = alias.split(":")[1];
+ dict[domain] = dict[domain] || [];
+ dict[domain].push(alias);
+ return dict;
+ }, {});
+ }
+
aliasEventsToDictionary(aliasEvents) { // m.room.alias events
const dict = {};
aliasEvents.forEach((event) => {
@@ -117,6 +146,16 @@ export default class AliasSettings extends React.Component {
return dict;
}
+ _getAltAliases() {
+ if (this.props.canonicalAliasEvent) {
+ const altAliases = this.props.canonicalAliasEvent.getContent().alt_aliases;
+ if (Array.isArray(altAliases)) {
+ return altAliases;
+ }
+ }
+ return [];
+ }
+
changeCanonicalAlias(alias) {
if (!this.props.canSetCanonicalAlias) return;
@@ -126,6 +165,8 @@ export default class AliasSettings extends React.Component {
});
const eventContent = {};
+ const altAliases = this._getAltAliases();
+ if (altAliases) eventContent["alt_aliases"] = altAliases;
if (alias) eventContent["alias"] = alias;
MatrixClientPeg.get().sendStateEvent(this.props.roomId, "m.room.canonical_alias",
diff --git a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
index 0c66503c43..480d55c044 100644
--- a/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/SecurityRoomSettingsTab.js
@@ -36,11 +36,12 @@ export default class SecurityRoomSettingsTab extends React.Component {
joinRule: "invite",
guestAccess: "can_join",
history: "shared",
+ hasAliases: false,
encrypted: false,
};
}
- componentWillMount(): void {
+ async componentWillMount(): void {
MatrixClientPeg.get().on("RoomState.events", this._onStateEvent);
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);
this.setState({joinRule, guestAccess, history, encrypted});
+ const hasAliases = await this._hasAliases();
+ this.setState({hasAliases});
}
_pullContentPropertyFromEvent(event, key, defaultValue) {
@@ -201,13 +204,25 @@ export default class SecurityRoomSettingsTab extends React.Component {
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() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const joinRule = this.state.joinRule;
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)
&& room.currentState.mayClientSendStateEvent("m.room.guest_access", client);
@@ -226,7 +241,7 @@ export default class SecurityRoomSettingsTab extends React.Component {
}
let aliasWarning = null;
- if (joinRule === 'public' && !hasAliases) {
+ if (joinRule === 'public' && !this.state.hasAliases) {
aliasWarning = (
diff --git a/src/components/views/toasts/VerificationRequestToast.js b/src/components/views/toasts/VerificationRequestToast.js
index a831505a05..4a881ae852 100644
--- a/src/components/views/toasts/VerificationRequestToast.js
+++ b/src/components/views/toasts/VerificationRequestToast.js
@@ -58,7 +58,7 @@ export default class VerificationRequestToast extends React.PureComponent {
_checkRequestIsPending = () => {
const {request} = this.props;
- if (request.ready || request.done || request.cancelled || request.observeOnly) {
+ if (!request.canAccept) {
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
}
};
diff --git a/src/createRoom.js b/src/createRoom.js
index c25b618dc6..07eaee3e8f 100644
--- a/src/createRoom.js
+++ b/src/createRoom.js
@@ -23,6 +23,7 @@ import dis from "./dispatcher";
import * as Rooms from "./Rooms";
import DMRoomMap from "./utils/DMRoomMap";
import {getAddressType} from "./UserAddress";
+import SettingsStore from "./settings/SettingsStore";
/**
* 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 rooms = roomIds.map(id => client.getRoom(id));
const suitableDMRooms = rooms.filter(r => {
@@ -169,12 +170,60 @@ export async function ensureDMExists(client, userId) {
}
return false;
});
- let roomId;
if (suitableDMRooms.length) {
- const room = suitableDMRooms[0];
- roomId = room.roomId;
+ return suitableDMRooms[0];
+ }
+}
+
+/*
+ * 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 {
- 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;
}
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 1f4d9829ad..04c0a7501a 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -5,21 +5,22 @@
"Failed to verify email address: make sure you clicked the link in the email": "Failed to verify email address: make sure you clicked the link in the email",
"Add Phone Number": "Add Phone Number",
"The platform you're on": "The platform you're on",
- "The version of Riot.im": "The version of Riot.im",
+ "The version of Riot": "The version of Riot",
"Whether or not you're logged in (we don't record your username)": "Whether or not you're logged in (we don't record your username)",
"Your language of choice": "Your language of choice",
"Which officially provided instance you are using, if any": "Which officially provided instance you are using, if any",
"Whether or not you're using the Richtext mode of the Rich Text Editor": "Whether or not you're using the Richtext mode of the Rich Text Editor",
- "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)",
"Your homeserver's URL": "Your homeserver's URL",
- "Your identity server's URL": "Your identity server's URL",
+ "Whether you're using Riot on a device where touch is the primary input mechanism": "Whether you're using Riot on a device where touch is the primary input mechanism",
+ "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)": "Whether or not you're using the 'breadcrumbs' feature (avatars above the room list)",
+ "Whether you're using Riot as an installed Progressive Web App": "Whether you're using Riot as an installed Progressive Web App",
"e.g. %(exampleValue)s": "e.g. %(exampleValue)s",
"Every page you use in the app": "Every page you use in the app",
"e.g.
": "e.g. ",
- "Your User Agent": "Your User Agent",
+ "Your user agent": "Your user agent",
"Your device resolution": "Your device resolution",
"Analytics": "Analytics",
- "The information being sent to us to help make Riot.im better includes:": "The information being sent to us to help make Riot.im better includes:",
+ "The information being sent to us to help make Riot better includes:": "The information being sent to us to help make Riot better includes:",
"Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.": "Where this page includes identifiable information, such as a room, user or group ID, that data is removed before being sent to the server.",
"Error": "Error",
"Unable to load! Check your network connectivity and try again.": "Unable to load! Check your network connectivity and try again.",
@@ -1198,11 +1199,12 @@
"This client does not support end-to-end encryption.": "This client does not support end-to-end encryption.",
"Messages in this room are not end-to-end encrypted.": "Messages in this room are not end-to-end encrypted.",
"Security": "Security",
- "Verify by emoji": "Verify by emoji",
- "Verify by comparing unique emoji.": "Verify by comparing unique emoji.",
+ "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.": "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.",
"Verify by scanning": "Verify by scanning",
"Ask %(displayName)s to scan your code:": "Ask %(displayName)s to scan your code:",
+ "Verify by emoji": "Verify by emoji",
"If you can't scan the code above, verify by comparing unique emoji.": "If you can't scan the code above, verify by comparing unique emoji.",
+ "Verify by comparing unique emoji.": "Verify by comparing unique emoji.",
"You've successfully verified %(displayName)s!": "You've successfully verified %(displayName)s!",
"Got it": "Got it",
"Verification timed out. Start verification again from their profile.": "Verification timed out. Start verification again from their profile.",
@@ -1240,8 +1242,12 @@
"%(name)s cancelled verifying": "%(name)s cancelled verifying",
"You accepted": "You accepted",
"%(name)s accepted": "%(name)s accepted",
+ "You declined": "You declined",
"You cancelled": "You cancelled",
+ "%(name)s declined": "%(name)s declined",
"%(name)s cancelled": "%(name)s cancelled",
+ "accepting …": "accepting …",
+ "declining …": "declining …",
"%(name)s wants to verify": "%(name)s wants to verify",
"You sent a verification request": "You sent a verification request",
"Error decrypting video": "Error decrypting video",
diff --git a/src/rageshake/submit-rageshake.js b/src/rageshake/submit-rageshake.js
index ed5a9e5946..091f64bf93 100644
--- a/src/rageshake/submit-rageshake.js
+++ b/src/rageshake/submit-rageshake.js
@@ -67,6 +67,18 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
userAgent = window.navigator.userAgent;
}
+ let installedPWA = "UNKNOWN";
+ try {
+ // Known to work at least for desktop Chrome
+ installedPWA = window.matchMedia('(display-mode: standalone)').matches;
+ } catch (e) { }
+
+ let touchInput = "UNKNOWN";
+ try {
+ // MDN claims broad support across browsers
+ touchInput = window.matchMedia('(pointer: coarse)').matches;
+ } catch (e) { }
+
const client = MatrixClientPeg.get();
console.log("Sending bug report.");
@@ -76,6 +88,8 @@ export default async function sendBugReport(bugReportEndpoint, opts) {
body.append('app', 'riot-web');
body.append('version', version);
body.append('user_agent', userAgent);
+ body.append('installed_pwa', installedPWA);
+ body.append('touch_input', touchInput);
if (client) {
body.append('user_id', client.credentials.userId);
diff --git a/src/usercontent/index.html b/src/usercontent/index.html
new file mode 100644
index 0000000000..90a0fe7c16
--- /dev/null
+++ b/src/usercontent/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+
diff --git a/src/usercontent/index.js b/src/usercontent/index.js
new file mode 100644
index 0000000000..b87ccb9dbb
--- /dev/null
+++ b/src/usercontent/index.js
@@ -0,0 +1,49 @@
+const params = window.location.search.substring(1).split('&');
+let lockOrigin;
+for (let i = 0; i < params.length; ++i) {
+ const parts = params[i].split('=');
+ if (parts[0] === 'origin') lockOrigin = decodeURIComponent(parts[1]);
+}
+
+function remoteRender(event) {
+ const data = event.data;
+
+ const img = document.createElement("img");
+ img.id = "img";
+ img.src = data.imgSrc;
+ img.style = data.imgStyle;
+
+ const a = document.createElement("a");
+ a.id = "a";
+ a.rel = "noopener";
+ a.target = "_blank";
+ 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);
+}
+
+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;
+}
+
+window.onmessage = function(e) {
+ if (e.origin === lockOrigin) {
+ if (e.data.blob) remoteRender(e);
+ else remoteSetTint(e);
+ }
+};
diff --git a/test/components/structures/MessagePanel-test.js b/test/components/structures/MessagePanel-test.js
index 59917057a5..e6332cf7f8 100644
--- a/test/components/structures/MessagePanel-test.js
+++ b/test/components/structures/MessagePanel-test.js
@@ -34,10 +34,15 @@ import Matrix from 'matrix-js-sdk';
const test_utils = require('../../test-utils');
const mockclock = require('../../mock-clock');
+import Adapter from "enzyme-adapter-react-16";
+import { configure, mount } from "enzyme";
+
import Velocity from 'velocity-animate';
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../src/contexts/RoomContext";
+configure({ adapter: new Adapter() });
+
let client;
const room = new Matrix.Room();
@@ -251,4 +256,111 @@ describe('MessagePanel', function() {
}, 100);
}, 100);
});
+
+ it('should collapse creation events', function() {
+ const mkEvent = test_utils.mkEvent;
+ const mkMembership = test_utils.mkMembership;
+ const roomId = "!someroom";
+ const alice = "@alice:example.org";
+ const ts0 = Date.now();
+ const events = [
+ mkEvent({
+ event: true,
+ type: "m.room.create",
+ room: roomId,
+ user: alice,
+ content: {
+ creator: alice,
+ room_version: "5",
+ predecessor: {
+ room_id: "!prevroom",
+ event_id: "$someevent",
+ },
+ },
+ ts: ts0,
+ }),
+ mkMembership({
+ event: true,
+ room: roomId,
+ user: alice,
+ target: {
+ userId: alice,
+ name: "Alice",
+ getAvatarUrl: () => {
+ return "avatar.jpeg";
+ },
+ },
+ ts: ts0 + 1,
+ mship: 'join',
+ name: 'Alice',
+ }),
+ mkEvent({
+ event: true,
+ type: "m.room.join_rules",
+ room: roomId,
+ user: alice,
+ content: {
+ "join_rule": "invite"
+ },
+ ts: ts0 + 2,
+ }),
+ mkEvent({
+ event: true,
+ type: "m.room.history_visibility",
+ room: roomId,
+ user: alice,
+ content: {
+ "history_visibility": "invited",
+ },
+ ts: ts0 + 3,
+ }),
+ mkEvent({
+ event: true,
+ type: "m.room.encryption",
+ room: roomId,
+ user: alice,
+ content: {
+ "algorithm": "m.megolm.v1.aes-sha2",
+ },
+ ts: ts0 + 4,
+ }),
+ mkMembership({
+ event: true,
+ room: roomId,
+ user: alice,
+ skey: "@bob:example.org",
+ target: {
+ userId: "@bob:example.org",
+ name: "Bob",
+ getAvatarUrl: () => {
+ return "avatar.jpeg";
+ },
+ },
+ ts: ts0 + 5,
+ mship: 'invite',
+ name: 'Bob',
+ }),
+ ];
+ const res = mount(
+ ,
+ );
+
+ // we expect that
+ // - the room creation event, the room encryption event, and Alice inviting Bob,
+ // should be outside of the room creation summary
+ // - all other events should be inside the room creation summary
+
+ const tiles = res.find(sdk.getComponent('views.rooms.EventTile'));
+
+ expect(tiles.at(0).props().mxEvent.getType()).toEqual("m.room.create");
+ expect(tiles.at(1).props().mxEvent.getType()).toEqual("m.room.encryption");
+
+ const summaryTiles = res.find(sdk.getComponent('views.elements.EventListSummary'));
+ const summaryTile = summaryTiles.at(0);
+
+ const summaryEventTiles = summaryTile.find(sdk.getComponent('views.rooms.EventTile'));
+ // every event except for the room creation, room encryption, and Bob's
+ // invite event should be in the event summary
+ expect(summaryEventTiles.length).toEqual(tiles.length - 3);
+ });
});
diff --git a/test/createRoom-test.js b/test/createRoom-test.js
new file mode 100644
index 0000000000..f7e8617c3f
--- /dev/null
+++ b/test/createRoom-test.js
@@ -0,0 +1,72 @@
+import {_waitForMember, canEncryptToAllUsers} from '../src/createRoom';
+import {EventEmitter} from 'events';
+
+/* Shorter timeout, we've got tests to run */
+const timeout = 30;
+
+describe("waitForMember", () => {
+ let client;
+
+ beforeEach(() => {
+ client = new EventEmitter();
+ });
+
+ it("resolves with false if the timeout is reached", (done) => {
+ _waitForMember(client, "", "", { timeout: 0 }).then((r) => {
+ expect(r).toBe(false);
+ done();
+ });
+ });
+
+ it("resolves with false if the timeout is reached, even if other RoomState.newMember events fire", (done) => {
+ const roomId = "!roomId:domain";
+ const userId = "@clientId:domain";
+ _waitForMember(client, roomId, userId, { timeout }).then((r) => {
+ expect(r).toBe(false);
+ done();
+ });
+ client.emit("RoomState.newMember", undefined, undefined, { roomId, userId: "@anotherClient:domain" });
+ });
+
+ it("resolves with true if RoomState.newMember fires", (done) => {
+ const roomId = "!roomId:domain";
+ const userId = "@clientId:domain";
+ _waitForMember(client, roomId, userId, { timeout }).then((r) => {
+ expect(r).toBe(true);
+ expect(client.listeners("RoomState.newMember").length).toBe(0);
+ done();
+ });
+ client.emit("RoomState.newMember", undefined, undefined, { roomId, userId });
+ });
+});
+
+describe("canEncryptToAllUsers", () => {
+ const trueUser = {
+ "@goodUser:localhost": {
+ "DEV1": {},
+ "DEV2": {},
+ },
+ };
+ const falseUser = {
+ "@badUser:localhost": {},
+ };
+
+ it("returns true if all devices have crypto", async (done) => {
+ const client = {
+ downloadKeys: async function(userIds) { return trueUser; },
+ };
+ const response = await canEncryptToAllUsers(client, ["@goodUser:localhost"]);
+ expect(response).toBe(true);
+ done();
+ });
+
+
+ it("returns false if not all users have crypto", async (done) => {
+ const client = {
+ downloadKeys: async function(userIds) { return {...trueUser, ...falseUser}; },
+ };
+ const response = await canEncryptToAllUsers(client, ["@goodUser:localhost", "@badUser:localhost"]);
+ expect(response).toBe(false);
+ done();
+ });
+});
diff --git a/test/test-utils.js b/test/test-utils.js
index fdd50a8792..d7aa9d5de9 100644
--- a/test/test-utils.js
+++ b/test/test-utils.js
@@ -51,7 +51,7 @@ export function createTestClient() {
getUserId: jest.fn().mockReturnValue("@userId:matrix.rog"),
getPushActionsForEvent: jest.fn(),
- getRoom: jest.fn().mockReturnValue(mkStubRoom()),
+ getRoom: jest.fn().mockImplementation(mkStubRoom),
getRooms: jest.fn().mockReturnValue([]),
getVisibleRooms: jest.fn().mockReturnValue([]),
getGroups: jest.fn().mockReturnValue([]),
@@ -111,7 +111,7 @@ export function mkEvent(opts) {
if (opts.skey) {
event.state_key = opts.skey;
} else if (["m.room.name", "m.room.topic", "m.room.create", "m.room.join_rules",
- "m.room.power_levels", "m.room.topic",
+ "m.room.power_levels", "m.room.topic", "m.room.history_visibility", "m.room.encryption",
"com.example.state"].indexOf(opts.type) !== -1) {
event.state_key = "";
}
diff --git a/yarn.lock b/yarn.lock
index b892ac44f6..1ed39459d0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5760,9 +5760,10 @@ mathml-tag-names@^2.0.1:
resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.1.tgz#6dff66c99d55ecf739ca53c492e626f1d12a33cc"
integrity sha512-pWB896KPGSGkp1XtyzRBftpTzwSOL0Gfk0wLvxt4f2mgzjY19o0LxJ3U25vNWTzsh7da+KTbuXQoQ3lOJZ8WHw==
-"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
- version "4.0.0"
- resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/21e4c597d9633aef606871cf9ffffaf039142be3"
+matrix-js-sdk@5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-5.0.0.tgz#dcbab35f1afdb35ef0364eb232e78e0fb7dc2a5b"
+ integrity sha512-A/aeE2Zn2OHq1n/9wIHCszrQZ7oXfThUHWi5Kz7illVCPUJ3JrZ31XVvx02k6vBasDcUtjAfZblHdTVN62cWLw==
dependencies:
"@babel/runtime" "^7.8.3"
another-json "^0.2.0"