diff --git a/.eslintignore.errorfiles b/.eslintignore.errorfiles index ffd398cb14..9973cfb120 100644 --- a/.eslintignore.errorfiles +++ b/.eslintignore.errorfiles @@ -1,6 +1,5 @@ # autogenerated file: run scripts/generate-eslint-error-ignore-file to update. -src/component-index.js src/components/structures/RoomDirectory.js src/components/structures/RoomStatusBar.js src/components/structures/RoomView.js @@ -10,7 +9,6 @@ src/components/structures/UploadBar.js src/components/views/avatars/BaseAvatar.js src/components/views/avatars/MemberAvatar.js src/components/views/create_room/RoomAlias.js -src/components/views/dialogs/DeactivateAccountDialog.js src/components/views/dialogs/SetPasswordDialog.js src/components/views/dialogs/UnknownDeviceDialog.js src/components/views/elements/AddressSelector.js @@ -31,7 +29,6 @@ src/components/views/rooms/MemberInfo.js src/components/views/rooms/MemberList.js src/components/views/rooms/RoomList.js src/components/views/rooms/RoomPreviewBar.js -src/components/views/rooms/SearchBar.js src/components/views/rooms/SearchResultTile.js src/components/views/settings/ChangeAvatar.js src/components/views/settings/ChangePassword.js @@ -44,7 +41,6 @@ src/notifications/ContentRules.js src/notifications/PushRuleVectorState.js src/PlatformPeg.js src/rageshake/rageshake.js -src/rageshake/submit-rageshake.js src/ratelimitedfunc.js src/Rooms.js src/Unread.js @@ -60,7 +56,6 @@ test/components/views/dialogs/InteractiveAuthDialog-test.js test/mock-clock.js test/notifications/ContentRules-test.js test/notifications/PushRuleVectorState-test.js -test/stores/RoomViewStore-test.js src/component-index.js test/end-to-end-tests/node_modules/ test/end-to-end-tests/riot/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fa02dc1ae3..07e478fa02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,254 @@ +Changes in [2.2.3](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3) (2020-03-17) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.3-rc.1...v2.2.3) + + * Upgrade JS SDK to 5.1.1 + * Add default on config setting to control call button in composer + [\#4228](https://github.com/matrix-org/matrix-react-sdk/pull/4228) + * Fix: make alternative addresses UX less confusing + [\#4226](https://github.com/matrix-org/matrix-react-sdk/pull/4226) + * Fix: best-effort to join room without canonical alias over federation from + room directory + [\#4211](https://github.com/matrix-org/matrix-react-sdk/pull/4211) + +Changes in [2.2.3-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.3-rc.1) (2020-03-11) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.1...v2.2.3-rc.1) + + * Update from Weblate + [\#4200](https://github.com/matrix-org/matrix-react-sdk/pull/4200) + * Revert "enable 4s when accepting a verification request" + [\#4198](https://github.com/matrix-org/matrix-react-sdk/pull/4198) + * Don't remount main split children on rhs collapse + [\#4197](https://github.com/matrix-org/matrix-react-sdk/pull/4197) + * Add fallback label for canonical alias events that dont change anything + [\#4195](https://github.com/matrix-org/matrix-react-sdk/pull/4195) + * Immediately switch to verification dialog when clicking [Continue] from new + session dialog + [\#4196](https://github.com/matrix-org/matrix-react-sdk/pull/4196) + * Enable 4S if needed when trying to verify or accepting verification + [\#4194](https://github.com/matrix-org/matrix-react-sdk/pull/4194) + * Remove extraneous tab stop from room tree view. + [\#4193](https://github.com/matrix-org/matrix-react-sdk/pull/4193) + * Remove v1 identity server fallbacks + [\#4191](https://github.com/matrix-org/matrix-react-sdk/pull/4191) + * Allow editing of alt_aliases according to MSC2432 + [\#4187](https://github.com/matrix-org/matrix-react-sdk/pull/4187) + * Update timeline rendering of aliases + [\#4189](https://github.com/matrix-org/matrix-react-sdk/pull/4189) + * Fix mark as read button for dark theme + [\#4190](https://github.com/matrix-org/matrix-react-sdk/pull/4190) + * Un-linkify version in settings + [\#4188](https://github.com/matrix-org/matrix-react-sdk/pull/4188) + * Make Mjolnir stop more robust + [\#4186](https://github.com/matrix-org/matrix-react-sdk/pull/4186) + * Fix secret sharing names to match spec + [\#4185](https://github.com/matrix-org/matrix-react-sdk/pull/4185) + * Share secrets with another device on request + [\#4172](https://github.com/matrix-org/matrix-react-sdk/pull/4172) + * Fall back to to_device verification if other user hasn't uploaded cross- + signing keys + [\#4181](https://github.com/matrix-org/matrix-react-sdk/pull/4181) + * Disable edits on redacted events + [\#4182](https://github.com/matrix-org/matrix-react-sdk/pull/4182) + * Use crypto.verification.request even when xsign is disabled + [\#4180](https://github.com/matrix-org/matrix-react-sdk/pull/4180) + * Reword the status for the currently indexing rooms. + [\#4084](https://github.com/matrix-org/matrix-react-sdk/pull/4084) + * Moved read receipts to the bottom of the message + [\#3892](https://github.com/matrix-org/matrix-react-sdk/pull/3892) + * Include a mark as read X under the scroll to unread button + [\#4159](https://github.com/matrix-org/matrix-react-sdk/pull/4159) + * Show the room presence indicator, even when cross-singing is enabled + [\#4178](https://github.com/matrix-org/matrix-react-sdk/pull/4178) + * Add local echo when clicking "Manually Verify" in unverified session dialog + [\#4179](https://github.com/matrix-org/matrix-react-sdk/pull/4179) + * link to matrix.org/security-disclosure-policy in help screen + [\#4129](https://github.com/matrix-org/matrix-react-sdk/pull/4129) + * only show verify button if user has uploaded cross-signing keys + [\#4174](https://github.com/matrix-org/matrix-react-sdk/pull/4174) + * Fix room alias references in topics + [\#4176](https://github.com/matrix-org/matrix-react-sdk/pull/4176) + * Fix not being able to start chats when you have no rooms + [\#4177](https://github.com/matrix-org/matrix-react-sdk/pull/4177) + * Disable registration flows on SSO servers + [\#4170](https://github.com/matrix-org/matrix-react-sdk/pull/4170) + * Don't group blank membership changes + [\#4160](https://github.com/matrix-org/matrix-react-sdk/pull/4160) + * Ensure the room list always triggers updates on itself + [\#4175](https://github.com/matrix-org/matrix-react-sdk/pull/4175) + * Fix composer touch bar flickering on keypress in Chrome + [\#4173](https://github.com/matrix-org/matrix-react-sdk/pull/4173) + * Document scrollpanel and BACAT scrolling + [\#4167](https://github.com/matrix-org/matrix-react-sdk/pull/4167) + * riot-desktop open SSO in browser so user doesn't have to auth twice + [\#4158](https://github.com/matrix-org/matrix-react-sdk/pull/4158) + * Lock login and registration buttons after submit + [\#4165](https://github.com/matrix-org/matrix-react-sdk/pull/4165) + * Suggest the server's results as lower quality in the invite dialog + [\#4149](https://github.com/matrix-org/matrix-react-sdk/pull/4149) + * Adjust scroll offset with relative scrolling + [\#4166](https://github.com/matrix-org/matrix-react-sdk/pull/4166) + * only automatically download in usercontent if user requested it + [\#4163](https://github.com/matrix-org/matrix-react-sdk/pull/4163) + * Fix having to decrypt & download in two steps + [\#4162](https://github.com/matrix-org/matrix-react-sdk/pull/4162) + * Use bash for release script + [\#4161](https://github.com/matrix-org/matrix-react-sdk/pull/4161) + * Revert to manual sorting for custom tag rooms + [\#4157](https://github.com/matrix-org/matrix-react-sdk/pull/4157) + * Fix the last char of people's names being cut off in the invite dialog + [\#4150](https://github.com/matrix-org/matrix-react-sdk/pull/4150) + * Add /whois SlashCommand to open UserInfo + [\#4154](https://github.com/matrix-org/matrix-react-sdk/pull/4154) + * word-break in pills and wrap the background correctly + [\#4155](https://github.com/matrix-org/matrix-react-sdk/pull/4155) + * don't show "This alias is available to use" if the alias is invalid + [\#4153](https://github.com/matrix-org/matrix-react-sdk/pull/4153) + * Don't ask to enable analytics when Do Not Track is enabled + [\#4098](https://github.com/matrix-org/matrix-react-sdk/pull/4098) + * Fix MELS not breaking on day boundaries regression + [\#4152](https://github.com/matrix-org/matrix-react-sdk/pull/4152) + * Fix Quote on search results page + [\#4151](https://github.com/matrix-org/matrix-react-sdk/pull/4151) + * Ensure errors when creating a DM are raised to the user + [\#4144](https://github.com/matrix-org/matrix-react-sdk/pull/4144) + * Add a Login button to startAnyRegistrationFlow + [\#3829](https://github.com/matrix-org/matrix-react-sdk/pull/3829) + * Use latest backup status directly rather than via state + [\#4147](https://github.com/matrix-org/matrix-react-sdk/pull/4147) + * Prefer account password variation of upgrading + [\#4146](https://github.com/matrix-org/matrix-react-sdk/pull/4146) + * Hide user avatars from screen readers in group and room user lists. + [\#4145](https://github.com/matrix-org/matrix-react-sdk/pull/4145) + * Room List sorting algorithms + [\#4085](https://github.com/matrix-org/matrix-react-sdk/pull/4085) + * Clear selected tags when disabling tag panel + [\#4143](https://github.com/matrix-org/matrix-react-sdk/pull/4143) + * Ignore cursor jumping shortcuts with shift + [\#4142](https://github.com/matrix-org/matrix-react-sdk/pull/4142) + * add local echo for clicking 'start verification' button + [\#4138](https://github.com/matrix-org/matrix-react-sdk/pull/4138) + * Fix formatting buttons not marking the composer as modified + [\#4141](https://github.com/matrix-org/matrix-react-sdk/pull/4141) + * Upgrade deps + [\#4136](https://github.com/matrix-org/matrix-react-sdk/pull/4136) + * Remove debug line from Analytics + [\#4137](https://github.com/matrix-org/matrix-react-sdk/pull/4137) + * Use the right function for creating binary verification QR codes + [\#4140](https://github.com/matrix-org/matrix-react-sdk/pull/4140) + * Ensure verification QR codes use the right buffer size + [\#4139](https://github.com/matrix-org/matrix-react-sdk/pull/4139) + * Don't prefix QR codes with the length of the static marker string + [\#4128](https://github.com/matrix-org/matrix-react-sdk/pull/4128) + * Solve fixed-width digit display in flowed text + [\#4127](https://github.com/matrix-org/matrix-react-sdk/pull/4127) + * Limit UserInfo Displayname to 3 lines to get rid of scrollbars + [\#4135](https://github.com/matrix-org/matrix-react-sdk/pull/4135) + +Changes in [2.2.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.1) (2020-03-04) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.0...v2.2.1) + + * Adjust scroll offset with relative scrolling + [\#4171](https://github.com/matrix-org/matrix-react-sdk/pull/4171) + * Disable registration flows on SSO servers + [\#4169](https://github.com/matrix-org/matrix-react-sdk/pull/4169) + +Changes in [2.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.0) (2020-03-02) +=================================================================================================== +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.2.0-rc.1...v2.2.0) + + * Upgrade JS SDK to 5.1.0 + * Ignore cursor jumping shortcuts with shift + [\#4142](https://github.com/matrix-org/matrix-react-sdk/pull/4142) + +Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.2.0-rc.1) (2020-02-26) +============================================================================================================= +[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.1...v2.2.0-rc.1) + + * Upgrade JS SDK to 5.1.0-rc.1 + * Fix message context menu breaking on invalid m.room.pinned_events event + [\#4133](https://github.com/matrix-org/matrix-react-sdk/pull/4133) + * Update from Weblate + [\#4134](https://github.com/matrix-org/matrix-react-sdk/pull/4134) + * Notify platform of language changes + [\#4121](https://github.com/matrix-org/matrix-react-sdk/pull/4121) + * Handle errors when previewing rooms more safely + [\#4132](https://github.com/matrix-org/matrix-react-sdk/pull/4132) + * Don't try to collapse zero events with a group + [\#4131](https://github.com/matrix-org/matrix-react-sdk/pull/4131) + * Don't print errors when the tab is used with no autocomplete present + [\#4130](https://github.com/matrix-org/matrix-react-sdk/pull/4130) + * Improve UI feedback while waiting for network + [\#4126](https://github.com/matrix-org/matrix-react-sdk/pull/4126) + * Ensure DMs tagged outside of account data work in the invite dialog + [\#4123](https://github.com/matrix-org/matrix-react-sdk/pull/4123) + * Show a warning dialog when user indicates a new session wasn't them + [\#4125](https://github.com/matrix-org/matrix-react-sdk/pull/4125) + * Show cancel events as hidden events if we wouldn't usually render them + [\#4120](https://github.com/matrix-org/matrix-react-sdk/pull/4120) + * Collapsed room list has unaligned room tiles #4030 version 2 + [\#4033](https://github.com/matrix-org/matrix-react-sdk/pull/4033) + * Check for cross-signing homeserver support + [\#4118](https://github.com/matrix-org/matrix-react-sdk/pull/4118) + * Don't leak if show_sas never comes (or already came) + [\#4119](https://github.com/matrix-org/matrix-react-sdk/pull/4119) + * Add verification request viewer in devtools + [\#4106](https://github.com/matrix-org/matrix-react-sdk/pull/4106) + * update phase when request prop changes + [\#4117](https://github.com/matrix-org/matrix-react-sdk/pull/4117) + * Handle file downloading locally in electron rather than sending to browser + [\#4113](https://github.com/matrix-org/matrix-react-sdk/pull/4113) + * Remove unused CIDER setting watcher + [\#4116](https://github.com/matrix-org/matrix-react-sdk/pull/4116) + * Use alt_aliases for pills and autocomplete + [\#4102](https://github.com/matrix-org/matrix-react-sdk/pull/4102) + * Add shortcuts for beginning / end of composer + [\#4108](https://github.com/matrix-org/matrix-react-sdk/pull/4108) + * Update from Weblate + [\#4115](https://github.com/matrix-org/matrix-react-sdk/pull/4115) + * Revert "Fix escaped markdown passing backslashes through" + [\#4114](https://github.com/matrix-org/matrix-react-sdk/pull/4114) + * Fix a couple of React warnings/errors + [\#4112](https://github.com/matrix-org/matrix-react-sdk/pull/4112) + * Fix two big DOM leaks which were locking Chrome solid. + [\#4111](https://github.com/matrix-org/matrix-react-sdk/pull/4111) + * Filter out empty strings when pasting IDs into the invite dialog + [\#4109](https://github.com/matrix-org/matrix-react-sdk/pull/4109) + * Remove buildkite pipeline + [\#4107](https://github.com/matrix-org/matrix-react-sdk/pull/4107) + * Use binary packing for verification QR codes + [\#4091](https://github.com/matrix-org/matrix-react-sdk/pull/4091) + * Fix several small bugs with the invite/DM dialog + [\#4099](https://github.com/matrix-org/matrix-react-sdk/pull/4099) + * ignore e2e tests node_modules during linting + [\#4103](https://github.com/matrix-org/matrix-react-sdk/pull/4103) + * Apply null-guard to room pills for when we can't fetch the room + [\#4104](https://github.com/matrix-org/matrix-react-sdk/pull/4104) + * Fix theme being overridden to light even after login is completed + [\#4105](https://github.com/matrix-org/matrix-react-sdk/pull/4105) + * Fix bug where SSSS could be overwritten if user never cross-signs + [\#4100](https://github.com/matrix-org/matrix-react-sdk/pull/4100) + * Accept canonical alias for pills + [\#4096](https://github.com/matrix-org/matrix-react-sdk/pull/4096) + * Fix: don't advertise ability to scan a QR code for verification + [\#4094](https://github.com/matrix-org/matrix-react-sdk/pull/4094) + * Fixes for printing event indexing stats. + [\#4082](https://github.com/matrix-org/matrix-react-sdk/pull/4082) + * Remove exec so release script continues + [\#4095](https://github.com/matrix-org/matrix-react-sdk/pull/4095) + * Use Persistent Storage where possible + [\#4092](https://github.com/matrix-org/matrix-react-sdk/pull/4092) + * Fix user page (missing null check) + [\#4088](https://github.com/matrix-org/matrix-react-sdk/pull/4088) + * Cancel verification request on dialog close + [\#4081](https://github.com/matrix-org/matrix-react-sdk/pull/4081) + * Fix various memory leaks due to method re-binding + [\#4093](https://github.com/matrix-org/matrix-react-sdk/pull/4093) + * Fix share message context menu option keyboard a11y + [\#4073](https://github.com/matrix-org/matrix-react-sdk/pull/4073) + Changes in [2.1.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v2.1.1) (2020-02-19) =================================================================================================== [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v2.1.0...v2.1.1) diff --git a/README.md b/README.md index 0fbed22030..d6fd6db1b7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ All code lands on the `develop` branch - `master` is only used for stable releas **Please file PRs against `develop`!!** Please follow the standard Matrix contributor's guide: -https://github.com/matrix-org/synapse/tree/master/CONTRIBUTING.rst +https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst Please follow the Matrix JS/React code style as per: https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md diff --git a/docs/jitsi.md b/docs/jitsi.md new file mode 100644 index 0000000000..779ef79d3a --- /dev/null +++ b/docs/jitsi.md @@ -0,0 +1,31 @@ +# Jitsi Wrapper + +**Note**: These are developer docs. Please consult your client's documentation for +instructions on setting up Jitsi. + +The react-sdk wraps all Jitsi call widgets in a local wrapper called `jitsi.html` +which takes several parameters: + +*Query string*: +* `widgetId`: The ID of the widget. This is needed for communication back to the + react-sdk. +* `parentUrl`: The URL of the parent window. This is also needed for + communication back to the react-sdk. + +*Hash/fragment (formatted as a query string)*: +* `conferenceDomain`: The domain to connect Jitsi Meet to. +* `conferenceId`: The room or conference ID to connect Jitsi Meet to. +* `isAudioOnly`: Boolean for whether this is a voice-only conference. May not + be present, should default to `false`. +* `displayName`: The display name of the user viewing the widget. May not + be present or could be null. +* `avatarUrl`: The HTTP(S) URL for the avatar of the user viewing the widget. May + not be present or could be null. +* `userId`: The MXID of the user viewing the widget. May not be present or could + be null. + +The react-sdk will assume that `jitsi.html` is at the path of wherever it is currently +being served. For example, `https://riot.im/develop/jitsi.html` or `vector://webapp/jitsi.html`. + +The `jitsi.html` wrapper can use the react-sdk's `WidgetApi` to communicate, making +it easier to actually implement the feature. diff --git a/docs/scrolling.md b/docs/scrolling.md new file mode 100644 index 0000000000..71329e5c32 --- /dev/null +++ b/docs/scrolling.md @@ -0,0 +1,28 @@ +# ScrollPanel + +## Updates + +During an onscroll event, we check whether we're getting close to the top or bottom edge of the loaded content. If close enough, we fire a request to load more through the callback passed in the `onFillRequest` prop. This returns a promise is passed down from `TimelinePanel`, where it will call paginate on the `TimelineWindow` and once the events are received back, update its state with the new events. This update trickles down to the `MessagePanel`, which rerenders all tiles and passed that to `ScrollPanel`. ScrollPanels `componentDidUpdate` method gets called, and we do the scroll housekeeping there (read below). Once the rerender has completed, the `setState` callback is called and we resolve the promise returned by `onFillRequest`. Now we check the DOM to see if we need more fill requests. + +## Prevent Shrinking + +ScrollPanel supports a mode to prevent it shrinking. This is used to prevent a jump when at the bottom of the timeline and people start and stop typing. It gets cleared automatically when 200px above the bottom of the timeline. + + +## BACAT (Bottom-Aligned, Clipped-At-Top) scrolling + +BACAT scrolling implements a different way of restoring the scroll position in the timeline while tiles out of view are changing height or tiles are being added or removed. It was added in https://github.com/matrix-org/matrix-react-sdk/pull/2842. + +The motivation for the changes is having noticed that setting scrollTop while scrolling tends to not work well, with it interrupting ongoing scrolling and also querying scrollTop reporting outdated values and consecutive scroll adjustments cancelling each out previous ones. This seems to be worse on macOS than other platforms, presumably because of a higher resolution in scroll events there. Also see https://github.com/vector-im/riot-web/issues/528. The BACAT approach allows to only have to change the scroll offset when adding or removing tiles. + +The approach taken instead is to vertically align the timeline tiles to the bottom of the scroll container (using flexbox) and give the timeline inside the scroll container an explicit height, initially set to a multiple of the PAGE_SIZE (400px at time of writing) as needed by the content. When scrolled up, we can compensate for anything that grew below the viewport by changing the height of the timeline to maintain what's currently visible in the viewport without adjusting the scrollTop and hence without jumping. + +For anything above the viewport growing or shrinking, we don't need to do anything as the timeline is bottom-aligned. We do need to update the height manually to keep all content visible as more is loaded. To maintain scroll position after the portion above the viewport changes height, we need to set the scrollTop, as we cannot balance it out with more height changes. We do this 100ms after the user has stopped scrolling, so setting scrollTop has not nasty side-effects. + +As of https://github.com/matrix-org/matrix-react-sdk/pull/4166, we are scrolling to compensate for height changes by calling `scrollBy(0, x)` rather than reading and than setting `scrollTop`, as reading `scrollTop` can (again, especially on macOS) easily return values that are out of sync with what is on the screen, probably because scrolling can be done [off the main thread](https://wiki.mozilla.org/Platform/GFX/APZ) in some circumstances. This seems to further prevent jumps. + +### How does it work? + +`componentDidUpdate` is called when a tile in the timeline is updated (as we rerender the whole timeline) or tiles are added or removed (see Updates section before). From here, `checkScroll` is called, which calls `_restoreSavedScrollState`. Now, we increase the timeline height if something below the viewport grew by adjusting `this._bottomGrowth`. `bottomGrowth` is the height added to the timeline (on top of the height from the number of pages calculated at the last `_updateHeight` run) to compensate for growth below the viewport. This is cleared during the next run of `_updateHeight`. Remember that the tiles in the timeline are aligned to the bottom. + +From `_restoreSavedScrollState` we also call `_updateHeight` which waits until the user stops scrolling for 100ms and then recalculates the amount of pages of 400px the timeline should be sized to, to be able to show all of its (newly added) content. We have to adjust the scroll offset (which is why we wait until scrolling has stopped) now because the space above the viewport has likely changed. diff --git a/package.json b/package.json index 624f2d6ecb..1ff0fb6f55 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "matrix-react-sdk", - "version": "2.1.1", + "version": "2.2.3", "description": "SDK for matrix.org using React", "author": "matrix.org", "repository": { @@ -40,15 +40,15 @@ "rethemendex": "res/css/rethemendex.sh", "clean": "rimraf lib", "build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types", - "build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js\" src", - "build:types": "tsc --emitDeclarationOnly", + "build:compile": "yarn reskindex && babel -d lib --verbose --extensions \".ts,.js,.tsx\" src", + "build:types": "tsc --emitDeclarationOnly --jsx react", "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style", "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", "lint:ts": "tslint --project ./tsconfig.json -t stylish", - "lint:types": "tsc --noEmit", + "lint:types": "tsc --noEmit --jsx react", "lint:style": "stylelint 'res/css/**/*.scss'", "test": "jest", "test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" @@ -72,7 +72,6 @@ "flux": "2.1.1", "focus-visible": "^5.0.2", "fuse.js": "^2.2.0", - "gemini-scrollbar": "github:matrix-org/gemini-scrollbar#91e1e566", "gfm.css": "^1.1.1", "glob-to-regexp": "^0.4.1", "highlight.js": "^9.15.8", @@ -84,6 +83,7 @@ "minimist": "^1.2.0", "pako": "^1.0.5", "png-chunks-extract": "^1.0.0", + "project-name-generator": "^2.1.7", "prop-types": "^15.5.8", "qrcode": "^1.4.4", "qrcode-react": "^0.1.16", @@ -93,7 +93,6 @@ "react-beautiful-dnd": "^4.0.1", "react-dom": "^16.9.0", "react-focus-lock": "^2.2.1", - "react-gemini-scrollbar": "github:matrix-org/react-gemini-scrollbar#9cf17f63b7c0b0ec5f31df27da0f82f7238dc594", "resize-observer-polyfill": "^1.5.0", "sanitize-html": "^1.18.4", "text-encoding-utf-8": "^1.0.1", @@ -118,6 +117,8 @@ "@babel/preset-typescript": "^7.7.4", "@babel/register": "^7.7.4", "@peculiar/webcrypto": "^1.0.22", + "@types/classnames": "^2.2.10", + "@types/react": "16.9", "babel-eslint": "^10.0.3", "babel-jest": "^24.9.0", "chokidar": "^3.3.1", diff --git a/release.sh b/release.sh index 3c28084bb7..23b8822041 100755 --- a/release.sh +++ b/release.sh @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash # # Script to perform a release of matrix-react-sdk. # @@ -11,6 +11,7 @@ cd `dirname $0` for i in matrix-js-sdk do + echo "Checking version of $i..." depver=`cat package.json | jq -r .dependencies[\"$i\"]` latestver=`yarn info -s $i dist-tags.next` if [ "$depver" != "$latestver" ] diff --git a/res/css/_common.scss b/res/css/_common.scss index e062e0bd73..ad64aced50 100644 --- a/res/css/_common.scss +++ b/res/css/_common.scss @@ -42,10 +42,15 @@ pre, code { font-size: 100% !important; } -.error, .warning { +.error, .warning, +.text-error, .text-warning { color: $warning-color; } +.text-success { + color: $accent-color; +} + b { // On Firefox, the default weight for `` is `bolder` which results in no bold // effect since we only have specific weights of our fonts available. @@ -202,37 +207,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus { transition: opacity 0.2s ease-in-out; } -/* XXX: critical hack to GeminiScrollbar to allow them to work in FF 42 and Chrome 48. - Stop the scrollbar view from pushing out the container's overall sizing, which causes - flexbox to adapt to the new size and cause the view to keep growing. - */ -.gm-scrollbar-container .gm-scroll-view { - position: absolute; -} - -/* Expand thumbs on hoverover */ -.gm-scrollbar { - border-radius: 5px !important; -} -.gm-scrollbar.-vertical { - width: 6px; - transition: width 120ms ease-out !important; -} -.gm-scrollbar.-vertical:hover, -.gm-scrollbar.-vertical:active { - width: 8px; - transition: width 120ms ease-out !important; -} -.gm-scrollbar.-horizontal { - height: 6px; - transition: height 120ms ease-out !important; -} -.gm-scrollbar.-horizontal:hover, -.gm-scrollbar.-horizontal:active { - height: 8px; - transition: height 120ms ease-out !important; -} - // These are magic constants which are excluded from tinting, to let themes // (which only have CSS, unlike skins) tell the app what their non-tinted // colourscheme is by inspecting the stylesheet DOM. diff --git a/res/css/_components.scss b/res/css/_components.scss index bc636eb3c6..6890a1ffd1 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -65,6 +65,7 @@ @import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_InviteDialog.scss"; +@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss"; @@ -177,7 +178,6 @@ @import "./views/rooms/_RoomTile.scss"; @import "./views/rooms/_RoomUpgradeWarningBar.scss"; @import "./views/rooms/_SearchBar.scss"; -@import "./views/rooms/_SearchableEntityList.scss"; @import "./views/rooms/_SendMessageComposer.scss"; @import "./views/rooms/_Stickers.scss"; @import "./views/rooms/_TopUnreadMessagesBar.scss"; diff --git a/res/css/structures/_GroupView.scss b/res/css/structures/_GroupView.scss index 517b8b1922..2575169664 100644 --- a/res/css/structures/_GroupView.scss +++ b/res/css/structures/_GroupView.scss @@ -180,10 +180,6 @@ limitations under the License. line-height: 2em; } -.mx_GroupView > .mx_MainSplit { - flex: 1; -} - .mx_GroupView_body { flex-grow: 1; } @@ -341,8 +337,8 @@ limitations under the License. display: none; } -.mx_GroupView_body .gm-scroll-view > * { - margin: 11px 50px 0px 68px; +.mx_GroupView_body .mx_AutoHideScrollbar_offset > * { + margin: 11px 50px 50px 68px; } .mx_GroupView_groupDesc textarea { @@ -370,7 +366,7 @@ limitations under the License. padding: 40px 20px; } -.mx_GroupView .mx_MemberInfo .gm-scroll-view > :not(.mx_MemberInfo_avatar) { +.mx_GroupView .mx_MemberInfo .mx_AutoHideScrollbar_offset > :not(.mx_MemberInfo_avatar) { padding-left: 16px; padding-right: 16px; } diff --git a/res/css/structures/_MainSplit.scss b/res/css/structures/_MainSplit.scss index 4d73953cd7..25e1153fce 100644 --- a/res/css/structures/_MainSplit.scss +++ b/res/css/structures/_MainSplit.scss @@ -18,6 +18,7 @@ limitations under the License. display: flex; flex-direction: row; min-width: 0; + height: 100%; } // move hit area 5px to the right so it doesn't overlap with the timeline scrollbar diff --git a/res/css/structures/_MatrixChat.scss b/res/css/structures/_MatrixChat.scss index f2ce7e1d5c..c5a5d50068 100644 --- a/res/css/structures/_MatrixChat.scss +++ b/res/css/structures/_MatrixChat.scss @@ -76,13 +76,6 @@ limitations under the License. flex: 1 1 0; min-width: 0; - /* Experimental fix for https://github.com/vector-im/vector-web/issues/947 - and https://github.com/vector-im/vector-web/issues/946. - Empirically this stops the MessagePanel's width exploding outwards when - gemini is in 'prevented' mode - */ - overflow-x: auto; - /* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari needed height 100% all the way down to the HomePage. Height does not have to be auto, empirically. diff --git a/res/css/structures/_MyGroups.scss b/res/css/structures/_MyGroups.scss index d25789ab94..36150c33a5 100644 --- a/res/css/structures/_MyGroups.scss +++ b/res/css/structures/_MyGroups.scss @@ -67,9 +67,6 @@ limitations under the License. } } - - - .mx_MyGroups_headerCard_header { font-weight: bold; margin-bottom: 10px; @@ -98,6 +95,11 @@ limitations under the License. display: flex; flex-direction: column; + overflow-y: auto; +} + +.mx_MyGroups_scrollable { + overflow-y: inherit; } .mx_MyGroups_placeholder { diff --git a/res/css/structures/_RoomDirectory.scss b/res/css/structures/_RoomDirectory.scss index 5ae8df7176..f3a7b0e243 100644 --- a/res/css/structures/_RoomDirectory.scss +++ b/res/css/structures/_RoomDirectory.scss @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -45,9 +46,8 @@ limitations under the License. } .mx_RoomDirectory_listheader { - display: flex; - margin-top: 12px; - margin-bottom: 12px; + display: block; + margin-top: 13px; } .mx_RoomDirectory_searchbox { @@ -64,7 +64,7 @@ limitations under the License. } .mx_RoomDirectory_table { - font-size: 14px; + font-size: 12px; color: $primary-fg-color; width: 100%; text-align: left; @@ -112,6 +112,7 @@ limitations under the License. .mx_RoomDirectory_name { display: inline-block; + font-size: 18px; font-weight: 600; } @@ -148,8 +149,8 @@ limitations under the License. padding: 0; } -.mx_RoomDirectory p { - font-size: 14px; +.mx_RoomDirectory > span { + font-size: 15px; margin-top: 0; .mx_AccessibleButton { diff --git a/res/css/structures/_TagPanel.scss b/res/css/structures/_TagPanel.scss index dddd2e324c..472831c0d9 100644 --- a/res/css/structures/_TagPanel.scss +++ b/res/css/structures/_TagPanel.scss @@ -23,6 +23,7 @@ limitations under the License. flex-direction: column; align-items: center; justify-content: space-between; + min-height: 0; } .mx_TagPanel_items_selected { @@ -57,6 +58,7 @@ limitations under the License. .mx_TagPanel .mx_TagPanel_scroller { flex-grow: 1; + width: 100%; } .mx_TagPanel .mx_TagPanel_tagTileContainer { diff --git a/res/css/structures/auth/_CompleteSecurity.scss b/res/css/structures/auth/_CompleteSecurity.scss index 2bf51d9574..601492d43c 100644 --- a/res/css/structures/auth/_CompleteSecurity.scss +++ b/res/css/structures/auth/_CompleteSecurity.scss @@ -37,6 +37,10 @@ limitations under the License. font-size: 15px; } +.mx_CompleteSecurity_waiting { + color: $notice-secondary-color; +} + .mx_CompleteSecurity_actionRow { display: flex; justify-content: flex-end; diff --git a/res/css/views/dialogs/_KeyboardShortcutsDialog.scss b/res/css/views/dialogs/_KeyboardShortcutsDialog.scss new file mode 100644 index 0000000000..638cacd41f --- /dev/null +++ b/res/css/views/dialogs/_KeyboardShortcutsDialog.scss @@ -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. +*/ + +.mx_KeyboardShortcutsDialog { + display: flex; + flex-wrap: wrap; + -webkit-box-orient: vertical; + -webkit-box-direction: normal; + flex-direction: column; + margin-bottom: -50px; + max-height: 1100px; // XXX: this may need adjusting when adding new shortcuts + + .mx_KeyboardShortcutsDialog_category { + width: 33.3333%; // 3 columns + margin: 0 0 40px; + + & > div { + padding-left: 5px; + } + } + + h3 { + margin: 0 0 10px; + } + + h5 { + margin: 15px 0 5px; + font-weight: normal; + } + + kbd { + padding: 5px; + border-radius: 4px; + background-color: $reaction-row-button-bg-color; + margin-right: 5px; + min-width: 20px; + text-align: center; + display: inline-block; + border: 1px solid $kbd-border-color; + box-shadow: 0 2px $kbd-border-color; + margin-bottom: 4px; + text-transform: capitalize; + + & + kbd { + margin-left: 5px; + } + } + + .mx_KeyboardShortcutsDialog_inline div { + display: inline; + } +} diff --git a/res/css/views/dialogs/_UnknownDeviceDialog.scss b/res/css/views/dialogs/_UnknownDeviceDialog.scss index 02e0fb1fe5..2b0f8dceca 100644 --- a/res/css/views/dialogs/_UnknownDeviceDialog.scss +++ b/res/css/views/dialogs/_UnknownDeviceDialog.scss @@ -14,14 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -// CSS voodoo to support a gemini-scrollbar for the contents of the dialog -.mx_Dialog_unknownDevice .mx_Dialog { - // ideally we'd shrink the height to fit when needed, but in practice this - // is a pain in the ass. plus might as well make the dialog big given how - // important it is. - height: 100%; -} - .mx_UnknownDeviceDialog { height: 100%; display: flex; @@ -44,6 +36,7 @@ limitations under the License. .mx_UnknownDeviceDialog .mx_Dialog_content { margin-bottom: 24px; + overflow-y: scroll; } .mx_UnknownDeviceDialog_deviceList > li { diff --git a/res/css/views/directory/_NetworkDropdown.scss b/res/css/views/directory/_NetworkDropdown.scss index d402f6c48f..106392f880 100644 --- a/res/css/views/directory/_NetworkDropdown.scss +++ b/res/css/views/directory/_NetworkDropdown.scss @@ -1,5 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,70 +15,149 @@ limitations under the License. */ .mx_NetworkDropdown { + height: 32px; position: relative; -} + width: max-content; + padding-right: 32px; + margin-left: auto; + margin-right: 9px; + margin-top: 12px; -.mx_NetworkDropdown_input { - position: relative; - border-radius: 3px; - border: 1px solid $strong-input-border-color; - font-weight: 300; - font-size: 13px; - user-select: none; -} - -.mx_NetworkDropdown_arrow { - border-color: $primary-fg-color transparent transparent; - border-style: solid; - border-width: 5px 5px 0; - display: block; - height: 0; - position: absolute; - right: 10px; - top: 16px; - width: 0; -} - -.mx_NetworkDropdown_networkoption { - height: 37px; - line-height: 37px; - padding-left: 8px; - padding-right: 8px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.mx_NetworkDropdown_networkoption img { - margin: 5px; - width: 25px; - vertical-align: middle; -} - -input.mx_NetworkDropdown_networkoption, input.mx_NetworkDropdown_networkoption:focus { - border: 0; - padding-top: 0; - padding-bottom: 0; + .mx_AccessibleButton { + width: max-content; + } } .mx_NetworkDropdown_menu { - position: absolute; - left: -1px; - right: -1px; - top: 100%; - z-index: 2; + min-width: 204px; margin: 0; - padding: 0px; - border-radius: 3px; - border: 1px solid $accent-color; + box-sizing: border-box; + border-radius: 4px; + border: 1px solid $dialog-close-fg-color; background-color: $primary-bg-color; } -.mx_NetworkDropdown_menu .mx_NetworkDropdown_networkoption:hover { - background-color: $focus-bg-color; -} - .mx_NetworkDropdown_menu_network { font-weight: bold; } +.mx_NetworkDropdown_server { + padding: 12px 0; + border-bottom: 1px solid $input-darker-fg-color; + + .mx_NetworkDropdown_server_title { + padding: 0 10px; + font-size: 15px; + font-weight: 600; + line-height: 20px; + margin-bottom: 4px; + + // remove server button + .mx_AccessibleButton { + position: absolute; + display: inline; + right: 12px; + height: 16px; + width: 16px; + margin-top: 4px; + + &::after { + content: ""; + position: absolute; + width: 16px; + height: 16px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/x.svg'); + background-color: $notice-primary-color; + } + } + } + + .mx_NetworkDropdown_server_subtitle { + padding: 0 10px; + font-size: 10px; + line-height: 14px; + margin-top: -4px; + margin-bottom: 4px; + color: $muted-fg-color; + } + + .mx_NetworkDropdown_server_network { + font-size: 12px; + line-height: 16px; + padding: 4px 10px; + cursor: pointer; + position: relative; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + + &[aria-checked=true]::after { + content: ""; + position: absolute; + width: 16px; + height: 16px; + right: 10px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/check.svg'); + background-color: $input-valid-border-color; + } + } +} + +.mx_NetworkDropdown_server_add, +.mx_NetworkDropdown_server_network { + &:hover { + background-color: $header-panel-bg-color; + } +} + +.mx_NetworkDropdown_server_add { + padding: 16px 10px 16px 32px; + position: relative; + border-radius: 0 0 4px 4px; + + &::before { + content: ""; + position: absolute; + width: 16px; + height: 16px; + left: 7px; + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/plus.svg'); + background-color: $muted-fg-color; + } +} + +.mx_NetworkDropdown_handle { + position: relative; + + &::after { + content: ""; + position: absolute; + width: 24px; + height: 24px; + right: -28px; // - (24 + 4) + mask-repeat: no-repeat; + mask-position: center; + mask-size: contain; + mask-image: url('$(res)/img/feather-customised/chevron-down.svg'); + background-color: $primary-fg-color; + } + + .mx_NetworkDropdown_handle_server { + color: $muted-fg-color; + font-size: 12px; + } +} + +.mx_NetworkDropdown_dialog .mx_Dialog { + width: 45vw; +} diff --git a/res/css/views/elements/_DirectorySearchBox.scss b/res/css/views/elements/_DirectorySearchBox.scss index ef944f6fa0..75ef3fbabd 100644 --- a/res/css/views/elements/_DirectorySearchBox.scss +++ b/res/css/views/elements/_DirectorySearchBox.scss @@ -18,7 +18,6 @@ limitations under the License. display: flex; padding-left: 9px; padding-right: 9px; - margin: 0 5px 0 0 !important; } .mx_DirectorySearchBox_joinButton { diff --git a/res/css/views/elements/_EditableItemList.scss b/res/css/views/elements/_EditableItemList.scss index 51fa4c4423..ef60f006cc 100644 --- a/res/css/views/elements/_EditableItemList.scss +++ b/res/css/views/elements/_EditableItemList.scss @@ -20,14 +20,21 @@ limitations under the License. } .mx_EditableItem { + display: flex; margin-bottom: 5px; - margin-left: 15px; } .mx_EditableItem_delete { + order: 3; margin-right: 5px; cursor: pointer; vertical-align: middle; + width: 14px; + height: 14px; + mask-image: url('$(res)/img/feather-customised/cancel.svg'); + mask-repeat: no-repeat; + background-color: $warning-color; + mask-size: 100%; } .mx_EditableItem_email { @@ -36,12 +43,19 @@ limitations under the License. .mx_EditableItem_promptText { margin-right: 10px; + order: 2; } .mx_EditableItem_confirmBtn { margin-right: 5px; } +.mx_EditableItem_item { + flex: auto 1 0; + order: 1; +} + .mx_EditableItemList_label { margin-bottom: 5px; } + diff --git a/res/css/views/elements/_RichText.scss b/res/css/views/elements/_RichText.scss index 73f0be291f..5066ee10f3 100644 --- a/res/css/views/elements/_RichText.scss +++ b/res/css/views/elements/_RichText.scss @@ -13,6 +13,11 @@ padding-left: 5px; } +a.mx_Pill { + word-break: break-all; + display: inline; +} + /* More specific to override `.markdown-body a` text-decoration */ .mx_EventTile_content .markdown-body a.mx_Pill { text-decoration: none; diff --git a/res/css/views/right_panel/_UserInfo.scss b/res/css/views/right_panel/_UserInfo.scss index 46d5e99d64..0e4b1bda9e 100644 --- a/res/css/views/right_panel/_UserInfo.scss +++ b/res/css/views/right_panel/_UserInfo.scss @@ -137,12 +137,19 @@ limitations under the License. font-size: 18px; line-height: 25px; flex: 1; - overflow-x: auto; - max-height: 50px; - display: flex; justify-content: center; align-items: center; + // limit to 2 lines, show an ellipsis if it overflows + // this looks webkit specific but is supported by Firefox 68+ + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + + overflow: hidden; + word-break: break-all; + text-overflow: ellipsis; + .mx_E2EIcon { margin: 5px; } diff --git a/res/css/views/room_settings/_AliasSettings.scss b/res/css/views/room_settings/_AliasSettings.scss index d4ae58e5b0..f8d92e7828 100644 --- a/res/css/views/room_settings/_AliasSettings.scss +++ b/res/css/views/room_settings/_AliasSettings.scss @@ -26,3 +26,21 @@ limitations under the License. outline: none; box-shadow: none; } + +.mx_AliasSettings { + summary { + cursor: pointer; + color: $accent-color; + font-weight: 600; + list-style: none; + + // list-style doesn't do it for webkit + &::-webkit-details-marker { + display: none; + } + } + + .mx_AliasSettings_localAliasHeader { + margin-top: 35px; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 9e683c5fe4..2f89c96d57 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -112,8 +112,6 @@ limitations under the License. .mx_EventTile_line, .mx_EventTile_reply { position: relative; - /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ - margin-right: 110px; padding-left: 65px; /* left gutter */ padding-top: 4px; padding-bottom: 2px; @@ -122,6 +120,13 @@ limitations under the License. line-height: 22px; } +.mx_RoomView_timeline_rr_enabled { + .mx_EventTile_line, .mx_EventTile_reply { + /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ + margin-right: 110px; + } +} + .mx_EventTile_bubbleContainer { display: grid; grid-template-columns: 1fr 100px; diff --git a/res/css/views/rooms/_RoomPreviewBar.scss b/res/css/views/rooms/_RoomPreviewBar.scss index 85b6916226..981cf06c69 100644 --- a/res/css/views/rooms/_RoomPreviewBar.scss +++ b/res/css/views/rooms/_RoomPreviewBar.scss @@ -33,6 +33,13 @@ limitations under the License. } } + h3, + .mx_RoomPreviewBar_message p { + // break-word, with fallback to break-all, which is wider supported + word-break: break-all; + word-break: break-word; + } + .mx_Spinner { width: auto; height: auto; diff --git a/res/css/views/rooms/_SearchableEntityList.scss b/res/css/views/rooms/_SearchableEntityList.scss deleted file mode 100644 index 37a663123d..0000000000 --- a/res/css/views/rooms/_SearchableEntityList.scss +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2016 OpenMarket Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -.mx_SearchableEntityList { - display: flex; - - flex-direction: column; -} - -.mx_SearchableEntityList_query { - font-family: $font-family; - border-radius: 3px; - border: 1px solid $input-border-color; - padding: 9px; - color: $primary-fg-color; - background-color: $primary-bg-color; - margin-left: 3px; - font-size: 15px; - margin-bottom: 8px; - width: 189px; -} - -.mx_SearchableEntityList_query::-moz-placeholder { - color: $primary-fg-color; - opacity: 0.5; - font-size: 12px; -} - -.mx_SearchableEntityList_query::-webkit-input-placeholder { - color: $primary-fg-color; - opacity: 0.5; - font-size: 12px; -} - -.mx_SearchableEntityList_listWrapper { - flex: 1; - - overflow-y: auto; -} - -.mx_SearchableEntityList_list { - display: table; - table-layout: fixed; - width: 100%; -} - -.mx_SearchableEntityList_list .mx_EntityTile_chevron { - display: none; -} - -.mx_SearchableEntityList_hrWrapper { - width: 100%; - flex: 0 0 auto; -} - -.mx_SearchableEntityList hr { - height: 1px; - border: 0px; - color: $primary-fg-color; - background-color: $primary-fg-color; - margin-right: 15px; - margin-top: 11px; - margin-bottom: 11px; -} diff --git a/res/css/views/rooms/_TopUnreadMessagesBar.scss b/res/css/views/rooms/_TopUnreadMessagesBar.scss index a3916f321a..28eddf1fa2 100644 --- a/res/css/views/rooms/_TopUnreadMessagesBar.scss +++ b/res/css/views/rooms/_TopUnreadMessagesBar.scss @@ -51,8 +51,30 @@ limitations under the License. position: absolute; width: 38px; height: 38px; - mask: url('$(res)/img/icon-jump-to-first-unread.svg'); + mask-image: url('$(res)/img/icon-jump-to-first-unread.svg'); mask-repeat: no-repeat; mask-position: 9px 13px; background: $roomtile-name-color; } + +.mx_TopUnreadMessagesBar_markAsRead { + display: block; + width: 18px; + height: 18px; + background: $primary-bg-color; + border: 1.3px solid $roomtile-name-color; + border-radius: 10px; + margin: 5px auto; +} + +.mx_TopUnreadMessagesBar_markAsRead::before { + content: ""; + position: absolute; + width: 18px; + height: 18px; + mask-image: url('$(res)/img/cancel.svg'); + mask-repeat: no-repeat; + mask-size: 10px; + mask-position: 4px 4px; + background: $roomtile-name-color; +} diff --git a/res/css/views/settings/tabs/_SettingsTab.scss b/res/css/views/settings/tabs/_SettingsTab.scss index 794c8106be..01a1d94956 100644 --- a/res/css/views/settings/tabs/_SettingsTab.scss +++ b/res/css/views/settings/tabs/_SettingsTab.scss @@ -45,9 +45,17 @@ limitations under the License. margin: 10px 100px 10px 0; // Align with the rest of the view } -.mx_SettingsTab_section .mx_SettingsFlag { - margin-right: 100px; - margin-bottom: 10px; +.mx_SettingsTab_section { + margin-bottom: 24px; + + .mx_SettingsFlag { + margin-right: 100px; + margin-bottom: 10px; + } + + &.mx_SettingsTab_subsectionText .mx_SettingsFlag { + margin-right: 0px !important; + } } .mx_SettingsTab_section .mx_SettingsFlag .mx_SettingsFlag_label { diff --git a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss index d003e175d9..be0af9123b 100644 --- a/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss +++ b/res/css/views/settings/tabs/user/_PreferencesUserSettingsTab.scss @@ -14,6 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ -.mx_PreferencesUserSettingsTab .mx_Field { - @mixin mx_Settings_fullWidthField; +.mx_PreferencesUserSettingsTab { + .mx_Field { + @mixin mx_Settings_fullWidthField; + } + + .mx_SettingsTab_section { + margin-bottom: 30px; + } } diff --git a/res/img/feather-customised/chevron-down.svg b/res/img/feather-customised/chevron-down.svg new file mode 100644 index 0000000000..bcb185ede7 --- /dev/null +++ b/res/img/feather-customised/chevron-down.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index a3515a9d99..bfa2272283 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -165,6 +165,8 @@ $reaction-row-button-hover-border-color: $header-panel-text-primary-color; $reaction-row-button-selected-bg-color: #1f6954; $reaction-row-button-selected-border-color: $accent-color; +$kbd-border-color: #000000; + $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; @@ -219,10 +221,6 @@ $user-tile-hover-bg-color: $header-panel-bg-color; filter: invert(1); } -.gm-scrollbar .thumb { - filter: invert(1); -} - // markdown overrides: .mx_EventTile_content .markdown-body pre:hover { border-color: #808080 !important; // inverted due to rules below diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index c868c81549..9bdd712e07 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -5,9 +5,12 @@ Arial empirically gets it right, hence prioritising Arial here. */ /* We fall through to Twemoji for emoji rather than falling through to native Emoji fonts (if any) to ensure cross-browser consistency */ -$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Arial, Helvetica, Sans-Serif; +/* Noto Color Emoji contains digits, in fixed-width, therefore causing + digits in flowed text to stand out. + TODO: Consider putting all emoji fonts to the end rather than the front. */ +$font-family: Nunito, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Arial, Helvetica, Sans-Serif, 'Noto Color Emoji'; -$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', 'Noto Color Emoji', Courier, monospace; +$monospace-font-family: Inconsolata, Twemoji, 'Apple Color Emoji', 'Segoe UI Emoji', Courier, monospace, 'Noto Color Emoji'; // unified palette // try to use these colors when possible @@ -287,6 +290,8 @@ $reaction-row-button-hover-border-color: $focus-bg-color; $reaction-row-button-selected-bg-color: #e9fff9; $reaction-row-button-selected-border-color: $accent-color; +$kbd-border-color: $reaction-row-button-border-color; + $tooltip-timeline-bg-color: $tagpanel-bg-color; $tooltip-timeline-fg-color: #ffffff; diff --git a/scripts/ci/end-to-end-tests.sh b/scripts/ci/end-to-end-tests.sh index 9bdb512940..2f907dffa2 100755 --- a/scripts/ci/end-to-end-tests.sh +++ b/scripts/ci/end-to-end-tests.sh @@ -6,16 +6,8 @@ set -ev -upload_logs() { - echo "--- Uploading logs" - buildkite-agent artifact upload "logs/**/*;synapse/installations/consent/homeserver.log" -} - handle_error() { EXIT_CODE=$? - if [ $TESTS_STARTED -eq 1 ]; then - upload_logs - fi exit $EXIT_CODE } diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js index 3d3d5af116..a4d53aea2f 100755 --- a/scripts/gen-i18n.js +++ b/scripts/gen-i18n.js @@ -237,7 +237,7 @@ const walkOpts = { const fullPath = path.join(root, fileStats.name); let trs; - if (fileStats.name.endsWith('.js')) { + if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.tsx')) { trs = getTranslationsJs(fullPath); } else if (fileStats.name.endsWith('.html')) { trs = getTranslationsOther(fullPath); diff --git a/scripts/reskindex.js b/scripts/reskindex.js index 81ab111f46..9fb0e1a7c0 100755 --- a/scripts/reskindex.js +++ b/scripts/reskindex.js @@ -8,11 +8,14 @@ var chokidar = require('chokidar'); var componentIndex = path.join('src', 'component-index.js'); var componentIndexTmp = componentIndex+".tmp"; var componentsDir = path.join('src', 'components'); -var componentGlob = '**/*.js'; +var componentJsGlob = '**/*.js'; +var componentTsGlob = '**/*.tsx'; var prevFiles = []; function reskindex() { - var files = glob.sync(componentGlob, {cwd: componentsDir}).sort(); + var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort(); + var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort(); + var files = [...tsFiles, ...jsFiles]; if (!filesHaveChanged(files, prevFiles)) { return; } @@ -36,7 +39,7 @@ function reskindex() { strm.write("let components = {};\n"); for (var i = 0; i < files.length; ++i) { - var file = files[i].replace('.js', ''); + var file = files[i].replace('.js', '').replace('.tsx', ''); var moduleName = (file.replace(/\//g, '.')); var importName = moduleName.replace(/\./g, "$"); @@ -79,7 +82,7 @@ if (!args.w) { } var watchDebouncer = null; -chokidar.watch(path.join(componentsDir, componentGlob)).on('all', (event, path) => { +chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { if (path === componentIndex) return; if (watchDebouncer) clearTimeout(watchDebouncer); watchDebouncer = setTimeout(reskindex, 1000); diff --git a/src/Analytics.js b/src/Analytics.js index 8eea47ea89..c96cfdefee 100644 --- a/src/Analytics.js +++ b/src/Analytics.js @@ -260,7 +260,6 @@ class Analytics { }); } catch (e) { console.error("Analytics error: ", e); - window.err = e; } } diff --git a/src/BasePlatform.js b/src/BasePlatform.js index 14e34a1f40..5d809eb28f 100644 --- a/src/BasePlatform.js +++ b/src/BasePlatform.js @@ -4,6 +4,7 @@ Copyright 2016 Aviral Dasgupta Copyright 2016 OpenMarket Ltd Copyright 2018 New Vector Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +19,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import {MatrixClient} from "matrix-js-sdk"; import dis from './dispatcher'; import BaseEventIndexManager from './indexing/BaseEventIndexManager'; @@ -162,4 +164,28 @@ export default class BasePlatform { getEventIndexingManager(): BaseEventIndexManager | null { return null; } + + setLanguage(preferredLangs: string[]) {} + + getSSOCallbackUrl(hsUrl: string, isUrl: string): URL { + const url = new URL(window.location.href); + // XXX: at this point, the fragment will always be #/login, which is no + // use to anyone. Ideally, we would get the intended fragment from + // MatrixChat.screenAfterLogin so that you could follow #/room links etc + // through an SSO login. + url.hash = ""; + url.searchParams.set("homeserver", hsUrl); + url.searchParams.set("identityServer", isUrl); + return url; + } + + /** + * Begin Single Sign On flows. + * @param {MatrixClient} mxClient the matrix client using which we should start the flow + * @param {"sso"|"cas"} loginType the type of SSO it is, CAS/SSO. + */ + startSingleSignOn(mxClient: MatrixClient, loginType: "sso"|"cas") { + const callbackUrl = this.getSSOCallbackUrl(mxClient.getHomeserverUrl(), mxClient.getIdentityServerUrl()); + window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType); // redirect to SSO + } } diff --git a/src/CallHandler.js b/src/CallHandler.js index 1551b57313..362db939a3 100644 --- a/src/CallHandler.js +++ b/src/CallHandler.js @@ -64,8 +64,8 @@ import SdkConfig from './SdkConfig'; import { showUnknownDeviceDialogForCalls } from './cryptodevices'; import WidgetUtils from './utils/WidgetUtils'; import WidgetEchoStore from './stores/WidgetEchoStore'; -import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore, { SettingLevel } from './settings/SettingsStore'; +import {generateHumanReadableId} from "./utils/NamingUtils"; global.mxCalls = { //room_id: MatrixCall @@ -143,7 +143,7 @@ function _setCallListeners(call) { "if you proceed without verifying them, it will be "+ "possible for someone to eavesdrop on your call.", ), - button: _t('Review Devices'), + button: _t('Review Sessions'), onFinished: function(confirmed) { if (confirmed) { const room = MatrixClientPeg.get().getRoom(call.roomId); @@ -395,32 +395,6 @@ function _onAction(payload) { } async function _startCallApp(roomId, type) { - // check for a working integration manager. Technically we could put - // the state event in anyway, but the resulting widget would then not - // work for us. Better that the user knows before everyone else in the - // room sees it. - const managers = IntegrationManagers.sharedInstance(); - let haveScalar = false; - if (managers.hasManager()) { - try { - const scalarClient = managers.getPrimaryManager().getScalarClient(); - await scalarClient.connect(); - haveScalar = scalarClient.hasCredentials(); - } catch (e) { - // ignore - } - } - - if (!haveScalar) { - const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); - - Modal.createTrackedDialog('Could not connect to the integration server', '', ErrorDialog, { - title: _t('Could not connect to the integration server'), - description: _t('A conference call could not be started because the integrations server is not available'), - }); - return; - } - dis.dispatch({ action: 'appsDrawer', show: true, @@ -456,31 +430,22 @@ async function _startCallApp(roomId, type) { return; } - // This inherits its poor naming from the field of the same name that goes into - // the event. It's just a random string to make the Jitsi URLs unique. - const widgetSessionId = Math.random().toString(36).substring(2); - const confId = room.roomId.replace(/[^A-Za-z0-9]/g, '') + widgetSessionId; - // NB. we can't just encodeURICompoent all of these because the $ signs need to be there - // (but currently the only thing that needs encoding is the confId) - const queryString = [ - 'confId='+encodeURIComponent(confId), - 'isAudioConf='+(type === 'voice' ? 'true' : 'false'), - 'displayName=$matrix_display_name', - 'avatarUrl=$matrix_avatar_url', - 'email=$matrix_user_id', - ].join('&'); + const confId = `JitsiConference_${generateHumanReadableId()}`; + const jitsiDomain = SdkConfig.get()['jitsi']['preferredDomain']; - let widgetUrl; - if (SdkConfig.get().integrations_jitsi_widget_url) { - // Try this config key. This probably isn't ideal as a way of discovering this - // URL, but this will at least allow the integration manager to not be hardcoded. - widgetUrl = SdkConfig.get().integrations_jitsi_widget_url + '?' + queryString; - } else { - const apiUrl = IntegrationManagers.sharedInstance().getPrimaryManager().apiUrl; - widgetUrl = apiUrl + '/widgets/jitsi.html?' + queryString; - } + let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl(); - const widgetData = { widgetSessionId }; + // TODO: Remove URL hacks when the mobile clients eventually support v2 widgets + const parsedUrl = new URL(widgetUrl); + parsedUrl.search = ''; // set to empty string to make the URL class use searchParams instead + parsedUrl.searchParams.set('confId', confId); + widgetUrl = parsedUrl.toString(); + + const widgetData = { + conferenceId: confId, + isAudioOnly: type === 'voice', + domain: jitsiDomain, + }; const widgetId = ( 'jitsi_' + diff --git a/src/CrossSigningManager.js b/src/CrossSigningManager.js index f19be03574..29eb3cb8be 100644 --- a/src/CrossSigningManager.js +++ b/src/CrossSigningManager.js @@ -21,6 +21,7 @@ import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase'; import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey'; import { _t } from './languageHandler'; import SettingsStore from './settings/SettingsStore'; +import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib"; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -95,6 +96,9 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { { keyInfo: info, checkPrivateKey: async (input) => { + if (!info.pubkey) { + return true; + } const key = await inputToKey(input); return MatrixClientPeg.get().checkSecretStoragePrivateKey(key, info.pubkey); }, @@ -125,10 +129,74 @@ async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) { return [name, key]; } +const onSecretRequested = async function({ + user_id: userId, + device_id: deviceId, + request_id: requestId, + name, + device_trust: deviceTrust, +}) { + console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); + const client = MatrixClientPeg.get(); + if (userId !== client.getUserId()) { + return; + } + if (!deviceTrust || !deviceTrust.isVerified()) { + console.log(`CrossSigningManager: Ignoring request from untrusted device ${deviceId}`); + return; + } + if (name.startsWith("m.cross_signing")) { + const callbacks = client.getCrossSigningCacheCallbacks(); + if (!callbacks.getCrossSigningKeyCache) return; + /* Explicit enumeration here is deliberate – never share the master key! */ + if (name === "m.cross_signing.self_signing") { + const key = await callbacks.getCrossSigningKeyCache("self_signing"); + if (!key) { + console.log( + `self_signing requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } else if (name === "m.cross_signing.user_signing") { + const key = await callbacks.getCrossSigningKeyCache("user_signing"); + if (!key) { + console.log( + `user_signing requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } + } else if (name === "m.megolm_backup.v1") { + const key = await client._crypto.getSessionBackupPrivateKey(); + if (!key) { + console.log( + `session backup key requested by ${deviceId}, but not found in cache`, + ); + } + return key && encodeBase64(key); + } + console.warn("onSecretRequested didn't recognise the secret named ", name); +}; + export const crossSigningCallbacks = { getSecretStorageKey, + onSecretRequested, }; +export async function promptForBackupPassphrase() { + let key; + + const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); + const { finished } = Modal.createTrackedDialog('Restore Backup', '', RestoreKeyBackupDialog, { + showSummary: false, keyCallback: k => key = k, + }, null, /* priority = */ false, /* static = */ true); + + const success = await finished; + if (!success) throw new Error("Key backup prompt cancelled"); + + return key; +} + /** * This helper should be used whenever you need to access secret storage. It * ensures that secret storage (and also cross-signing since they each depend on @@ -185,6 +253,7 @@ export async function accessSecretStorage(func = async () => { }, force = false) throw new Error("Cross-signing key upload auth canceled"); } }, + getBackupPassphrase: promptForBackupPassphrase, }); } diff --git a/src/DeviceListener.js b/src/DeviceListener.js index 4e7bc8470d..7878a1a670 100644 --- a/src/DeviceListener.js +++ b/src/DeviceListener.js @@ -50,6 +50,7 @@ export default class DeviceListener { MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().on('accountData', this._onAccountData); this._recheck(); } @@ -58,6 +59,7 @@ export default class DeviceListener { MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated); MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged); MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged); + MatrixClientPeg.get().removeListener('accountData', this._onAccountData); } this._dismissed.clear(); } @@ -87,6 +89,13 @@ export default class DeviceListener { this._recheck(); } + _onAccountData = (ev) => { + // User may have migrated SSSS to symmetric, in which case we can dismiss that toast + if (ev.getType().startsWith('m.secret_storage.key.')) { + this._recheck(); + } + } + // The server doesn't tell us when key backup is set up, so we poll // & cache the result async _getKeyBackupInfo() { @@ -99,11 +108,18 @@ export default class DeviceListener { } async _recheck() { - if (!SettingsStore.isFeatureEnabled("feature_cross_signing")) return; const cli = MatrixClientPeg.get(); + if ( + !SettingsStore.isFeatureEnabled("feature_cross_signing") || + !await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") + ) return; + if (!cli.isCryptoEnabled()) return; - if (!cli.getCrossSigningId()) { + + const crossSigningReady = await cli.isCrossSigningReady(); + + if (!crossSigningReady) { if (this._dismissedThisDeviceToast) { ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); return; @@ -143,6 +159,19 @@ export default class DeviceListener { } } return; + } else if (await cli.secretStorageKeyNeedsUpgrade()) { + if (this._dismissedThisDeviceToast) { + ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); + return; + } + + ToastStore.sharedInstance().addOrReplaceToast({ + key: THIS_DEVICE_TOAST_KEY, + title: _t("Encryption upgrade available"), + icon: "verification_warning", + props: {kind: 'upgrade_encryption'}, + component: sdk.getComponent("toasts.SetupEncryptionToast"), + }); } else { ToastStore.sharedInstance().dismissToast(THIS_DEVICE_TOAST_KEY); } diff --git a/src/FromWidgetPostMessageApi.js b/src/FromWidgetPostMessageApi.js index 64caba0fdf..ea76c85643 100644 --- a/src/FromWidgetPostMessageApi.js +++ b/src/FromWidgetPostMessageApi.js @@ -24,6 +24,8 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import RoomViewStore from "./stores/RoomViewStore"; import {IntegrationManagers} from "./integrations/IntegrationManagers"; import SettingsStore from "./settings/SettingsStore"; +import {Capability, KnownWidgetActions} from "./widgets/WidgetApi"; +import SdkConfig from "./SdkConfig"; const WIDGET_API_VERSION = '0.0.2'; // Current API version const SUPPORTED_WIDGET_API_VERSIONS = [ @@ -213,11 +215,18 @@ export default class FromWidgetPostMessageApi { const data = event.data.data; const val = data.value; - if (ActiveWidgetStore.widgetHasCapability(widgetId, 'm.always_on_screen')) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) { ActiveWidgetStore.setWidgetPersistence(widgetId, val); } } else if (action === 'get_openid') { // Handled by caller + } else if (action === KnownWidgetActions.GetRiotWebConfig) { + if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.GetRiotWebConfig)) { + this.sendResponse(event, { + api: INBOUND_API_NAME, + config: SdkConfig.get(), + }); + } } else { console.warn('Widget postMessage event unhandled'); this.sendError(event, {message: 'The postMessage was unhandled'}); diff --git a/src/HtmlUtils.js b/src/HtmlUtils.js index 7dd68e5c61..a58ea25c8a 100644 --- a/src/HtmlUtils.js +++ b/src/HtmlUtils.js @@ -23,7 +23,6 @@ import ReplyThread from "./components/views/elements/ReplyThread"; import React from 'react'; import sanitizeHtml from 'sanitize-html'; -import highlight from 'highlight.js'; import * as linkify from 'linkifyjs'; import linkifyMatrix from './linkify-matrix'; import _linkifyElement from 'linkifyjs/element'; @@ -467,11 +466,12 @@ export function bodyToHtml(content, highlights, opts={}) { /** * Linkifies the given string. This is a wrapper around 'linkifyjs/string'. * - * @param {string} str - * @returns {string} + * @param {string} str string to linkify + * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options + * @returns {string} Linkified string */ -export function linkifyString(str) { - return _linkifyString(str); +export function linkifyString(str, options = linkifyMatrix.options) { + return _linkifyString(str, options); } /** @@ -489,10 +489,11 @@ export function linkifyElement(element, options = linkifyMatrix.options) { * Linkify the given string and sanitize the HTML afterwards. * * @param {string} dirtyHtml The HTML string to sanitize and linkify + * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @returns {string} */ -export function linkifyAndSanitizeHtml(dirtyHtml) { - return sanitizeHtml(linkifyString(dirtyHtml), sanitizeHtmlParams); +export function linkifyAndSanitizeHtml(dirtyHtml, options = linkifyMatrix.options) { + return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); } /** diff --git a/src/IdentityAuthClient.js b/src/IdentityAuthClient.js index 72432b9a44..4a830d6506 100644 --- a/src/IdentityAuthClient.js +++ b/src/IdentityAuthClient.js @@ -181,24 +181,12 @@ export default class IdentityAuthClient { } async registerForToken(check=true) { - try { - const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); - // XXX: The spec is `token`, but we used `access_token` for a Sydent release. - const { access_token: accessToken, token } = - await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); - const identityAccessToken = token ? token : accessToken; - if (check) await this._checkToken(identityAccessToken); - return identityAccessToken; - } catch (e) { - if (e.cors === "rejected" || e.httpStatus === 404) { - // Assume IS only supports deprecated v1 API for now - // TODO: Remove this path once v2 is only supported version - // See https://github.com/vector-im/riot-web/issues/10443 - console.warn("IS doesn't support v2 auth"); - this.authEnabled = false; - return; - } - throw e; - } + const hsOpenIdToken = await MatrixClientPeg.get().getOpenIdToken(); + // XXX: The spec is `token`, but we used `access_token` for a Sydent release. + const { access_token: accessToken, token } = + await this._matrixClient.registerWithIdentityServer(hsOpenIdToken); + const identityAccessToken = token ? token : accessToken; + if (check) await this._checkToken(identityAccessToken); + return identityAccessToken; } } diff --git a/src/Keyboard.js b/src/Keyboard.ts similarity index 92% rename from src/Keyboard.js rename to src/Keyboard.ts index 478d75acc1..817d0a0b97 100644 --- a/src/Keyboard.js +++ b/src/Keyboard.ts @@ -36,10 +36,12 @@ export const Key = { CONTEXT_MENU: "ContextMenu", COMMA: ",", + PERIOD: ".", LESS_THAN: "<", GREATER_THAN: ">", BACKTICK: "`", SPACE: " ", + SLASH: "/", A: "a", B: "b", C: "c", @@ -68,8 +70,9 @@ export const Key = { Z: "z", }; +export const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; + export function isOnlyCtrlOrCmdKeyEvent(ev) { - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; if (isMac) { return ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey; } else { @@ -78,7 +81,6 @@ export function isOnlyCtrlOrCmdKeyEvent(ev) { } export function isOnlyCtrlOrCmdIgnoreShiftKeyEvent(ev) { - const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; if (isMac) { return ev.metaKey && !ev.altKey && !ev.ctrlKey; } else { diff --git a/src/Lifecycle.js b/src/Lifecycle.js index 72cd84bfd9..b9fbf4f1bc 100644 --- a/src/Lifecycle.js +++ b/src/Lifecycle.js @@ -313,7 +313,7 @@ async function _restoreFromLocalStorage(opts) { } } -function _handleLoadSessionFailure(e) { +async function _handleLoadSessionFailure(e) { console.error("Unable to load session", e); const SessionRestoreErrorDialog = @@ -323,16 +323,15 @@ function _handleLoadSessionFailure(e) { error: e.message, }); - return modal.finished.then(([success]) => { - if (success) { - // user clicked continue. - _clearStorage(); - return false; - } + const [success] = await modal.finished; + if (success) { + // user clicked continue. + await _clearStorage(); + return false; + } - // try, try again - return loadSession(); - }); + // try, try again + return loadSession(); } /** diff --git a/src/Login.js b/src/Login.js index d9ce8adaaa..1590e5ac28 100644 --- a/src/Login.js +++ b/src/Login.js @@ -3,6 +3,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2018 New Vector Ltd Copyright 2019 Michael Telatynski <7t3chguy@gmail.com> +Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,8 +20,6 @@ limitations under the License. import Matrix from "matrix-js-sdk"; -import url from 'url'; - export default class Login { constructor(hsUrl, isUrl, fallbackHsUrl, opts) { this._hsUrl = hsUrl; @@ -29,6 +28,7 @@ export default class Login { this._currentFlowIndex = 0; this._flows = []; this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName; + this._tempClient = null; // memoize } getHomeserverUrl() { @@ -40,10 +40,12 @@ export default class Login { } setHomeserverUrl(hsUrl) { + this._tempClient = null; // clear memoization this._hsUrl = hsUrl; } setIdentityServerUrl(isUrl) { + this._tempClient = null; // clear memoization this._isUrl = isUrl; } @@ -52,8 +54,9 @@ export default class Login { * requests. * @returns {MatrixClient} */ - _createTemporaryClient() { - return Matrix.createClient({ + createTemporaryClient() { + if (this._tempClient) return this._tempClient; // use memoization + return this._tempClient = Matrix.createClient({ baseUrl: this._hsUrl, idBaseUrl: this._isUrl, }); @@ -61,7 +64,7 @@ export default class Login { getFlows() { const self = this; - const client = this._createTemporaryClient(); + const client = this.createTemporaryClient(); return client.loginFlows().then(function(result) { self._flows = result.flows; self._currentFlowIndex = 0; @@ -139,21 +142,6 @@ export default class Login { throw error; }); } - - getSsoLoginUrl(loginType) { - const client = this._createTemporaryClient(); - const parsedUrl = url.parse(window.location.href, true); - - // XXX: at this point, the fragment will always be #/login, which is no - // use to anyone. Ideally, we would get the intended fragment from - // MatrixChat.screenAfterLogin so that you could follow #/room links etc - // through an SSO login. - parsedUrl.hash = ""; - - parsedUrl.query["homeserver"] = client.getHomeserverUrl(); - parsedUrl.query["identityServer"] = client.getIdentityServerUrl(); - return client.getSsoLoginUrl(url.format(parsedUrl), loginType); - } } diff --git a/src/Registration.js b/src/Registration.js index ac8baa3cca..ca162bac03 100644 --- a/src/Registration.js +++ b/src/Registration.js @@ -39,6 +39,8 @@ export const SAFE_LOCALPART_REGEX = /^[a-z0-9=_\-./]+$/; * If true, goes to the home page if the user cancels the action * @param {bool} options.go_welcome_on_cancel * If true, goes to the welcome page if the user cancels the action + * @param {bool} options.screen_after + * If present the screen to redirect to after a successful login or register. */ export async function startAnyRegistrationFlow(options) { if (options === undefined) options = {}; @@ -66,13 +68,21 @@ export async function startAnyRegistrationFlow(options) { // }); //} else { const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); - Modal.createTrackedDialog('Registration required', '', QuestionDialog, { - title: _t("Registration Required"), - description: _t("You need to register to do this. Would you like to register now?"), - button: _t("Register"), + const modal = Modal.createTrackedDialog('Registration required', '', QuestionDialog, { + hasCancelButton: true, + quitOnly: true, + title: _t("Sign In or Create Account"), + description: _t("Use your account or create a new one to continue."), + button: _t("Create Account"), + extraButtons: [ + , + ], onFinished: (proceed) => { if (proceed) { - dis.dispatch({action: 'start_registration'}); + dis.dispatch({action: 'start_registration', screenAfterLogin: options.screen_after}); } else if (options.go_home_on_cancel) { dis.dispatch({action: 'view_home_page'}); } else if (options.go_welcome_on_cancel) { @@ -101,4 +111,3 @@ export async function startAnyRegistrationFlow(options) { // } // throw new Error("Register request succeeded when it should have returned 401!"); // } - diff --git a/src/SdkConfig.ts b/src/SdkConfig.ts index 8177a6c5b8..34f3402334 100644 --- a/src/SdkConfig.ts +++ b/src/SdkConfig.ts @@ -26,6 +26,13 @@ export const DEFAULTS: ConfigOptions = { integrations_rest_url: "https://scalar.vector.im/api", // Where to send bug reports. If not specified, bugs cannot be sent. bug_report_endpoint_url: null, + // Jitsi conference options + jitsi: { + // Default conference domain + preferredDomain: "jitsi.riot.im", + // Default Jitsi Meet API location + externalApiUrl: "https://jitsi.riot.im/libs/external_api.min.js", + }, }; export default class SdkConfig { diff --git a/src/Searching.js b/src/Searching.js index a5d945f64b..663328fe41 100644 --- a/src/Searching.js +++ b/src/Searching.js @@ -87,6 +87,13 @@ async function localSearch(searchTerm, roomId = undefined) { searchArgs.room_id = roomId; } + const emptyResult = { + results: [], + highlights: [], + }; + + if (searchTerm === "") return emptyResult; + const eventIndex = EventIndexPeg.get(); const localResult = await eventIndex.search(searchArgs); @@ -97,11 +104,6 @@ async function localSearch(searchTerm, roomId = undefined) { }, }; - const emptyResult = { - results: [], - highlights: [], - }; - const result = MatrixClientPeg.get()._processRoomEventsSearch( emptyResult, response); diff --git a/src/SlashCommands.js b/src/SlashCommands.js index b39b8fb9ac..d306978f78 100644 --- a/src/SlashCommands.js +++ b/src/SlashCommands.js @@ -893,6 +893,26 @@ export const CommandMap = { }, category: CommandCategories.advanced, }), + + whois: new Command({ + name: "whois", + description: _td("Displays information about a user"), + args: '', + runFn: function(roomId, userId) { + if (!userId || !userId.startsWith("@") || !userId.includes(":")) { + return reject(this.getUsage()); + } + + const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); + + dis.dispatch({ + action: 'view_user', + member: member || {userId}, + }); + return success(); + }, + category: CommandCategories.advanced, + }), }; /* eslint-enable babel/no-invalid-this */ diff --git a/src/TextForEvent.js b/src/TextForEvent.js index d4003058c8..6b1c1dcd2d 100644 --- a/src/TextForEvent.js +++ b/src/TextForEvent.js @@ -127,6 +127,13 @@ function textForRoomNameEvent(ev) { if (!ev.getContent().name || ev.getContent().name.trim().length === 0) { return _t('%(senderDisplayName)s removed the room name.', {senderDisplayName}); } + if (ev.getPrevContent().name) { + return _t('%(senderDisplayName)s changed the room name from %(oldRoomName)s to %(newRoomName)s.', { + senderDisplayName, + oldRoomName: ev.getPrevContent().name, + newRoomName: ev.getContent().name, + }); + } return _t('%(senderDisplayName)s changed the room name to %(roomName)s.', { senderDisplayName, roomName: ev.getContent().name, @@ -269,85 +276,55 @@ function textForMessageEvent(ev) { return message; } -function textForRoomAliasesEvent(ev) { - // An alternative implementation of this as a first-class event can be found at - // https://github.com/matrix-org/matrix-react-sdk/blob/dc7212ec2bd12e1917233ed7153b3e0ef529a135/src/components/views/messages/RoomAliasesEvent.js - // This feels a bit overkill though, and it's not clear the i18n really needs it - // so instead it's landing as a simple textual event. - - const maxShown = 3; - - const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); - const oldAliases = ev.getPrevContent().aliases || []; - const newAliases = ev.getContent().aliases || []; - - const addedAliases = newAliases.filter((x) => !oldAliases.includes(x)); - const removedAliases = oldAliases.filter((x) => !newAliases.includes(x)); - - if (!addedAliases.length && !removedAliases.length) { - return ''; - } - - if (addedAliases.length && !removedAliases.length) { - if (addedAliases.length > maxShown) { - return _t("%(senderName)s added %(addedAddresses)s and %(count)s other addresses to this room", { - senderName: senderName, - count: addedAliases.length - maxShown, - addedAddresses: addedAliases.slice(0, maxShown).join(', '), - }); - } - return _t('%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.', { - senderName: senderName, - count: addedAliases.length, - addedAddresses: addedAliases.join(', '), - }); - } else if (!addedAliases.length && removedAliases.length) { - if (removedAliases.length > maxShown) { - return _t("%(senderName)s removed %(removedAddresses)s and %(count)s other addresses from this room", { - senderName: senderName, - count: removedAliases.length - maxShown, - removedAddresses: removedAliases.slice(0, maxShown).join(', '), - }); - } - return _t('%(senderName)s removed %(count)s %(removedAddresses)s as addresses for this room.', { - senderName: senderName, - count: removedAliases.length, - removedAddresses: removedAliases.join(', '), - }); - } else { - const combined = addedAliases.length + removedAliases.length; - if (combined > maxShown) { - return _t("%(senderName)s removed %(countRemoved)s and added %(countAdded)s addresses to this room", { - senderName: senderName, - countAdded: addedAliases.length, - countRemoved: removedAliases.length, - }); - } - return _t( - '%(senderName)s added %(addedAddresses)s and removed %(removedAddresses)s as addresses for this room.', { - senderName: senderName, - addedAddresses: addedAliases.join(', '), - removedAddresses: removedAliases.join(', '), - }, - ); - } -} - function textForCanonicalAliasEvent(ev) { const senderName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender(); const oldAlias = ev.getPrevContent().alias; + const oldAltAliases = ev.getPrevContent().alt_aliases || []; const newAlias = ev.getContent().alias; + const newAltAliases = ev.getContent().alt_aliases || []; + const removedAltAliases = oldAltAliases.filter(alias => !newAltAliases.includes(alias)); + const addedAltAliases = newAltAliases.filter(alias => !oldAltAliases.includes(alias)); - if (newAlias) { - return _t('%(senderName)s set the main address for this room to %(address)s.', { - senderName: senderName, - address: ev.getContent().alias, - }); - } else if (oldAlias) { - return _t('%(senderName)s removed the main address for this room.', { + if (!removedAltAliases.length && !addedAltAliases.length) { + if (newAlias) { + return _t('%(senderName)s set the main address for this room to %(address)s.', { + senderName: senderName, + address: ev.getContent().alias, + }); + } else if (oldAlias) { + return _t('%(senderName)s removed the main address for this room.', { + senderName: senderName, + }); + } + } else if (newAlias === oldAlias) { + if (addedAltAliases.length && !removedAltAliases.length) { + return _t('%(senderName)s added the alternative addresses %(addresses)s for this room.', { + senderName: senderName, + addresses: addedAltAliases.join(", "), + count: addedAltAliases.length, + }); + } if (removedAltAliases.length && !addedAltAliases.length) { + return _t('%(senderName)s removed the alternative addresses %(addresses)s for this room.', { + senderName: senderName, + addresses: removedAltAliases.join(", "), + count: removedAltAliases.length, + }); + } if (removedAltAliases.length && addedAltAliases.length) { + return _t('%(senderName)s changed the alternative addresses for this room.', { + senderName: senderName, + }); + } + } else { + // both alias and alt_aliases where modified + return _t('%(senderName)s changed the main and alternative addresses for this room.', { senderName: senderName, }); } + // in case there is no difference between the two events, + // say something as we can't simply hide the tile from here + return _t('%(senderName)s changed the addresses for this room.', { + senderName: senderName, + }); } function textForCallAnswerEvent(event) { @@ -612,7 +589,6 @@ const handlers = { }; const stateHandlers = { - 'm.room.aliases': textForRoomAliasesEvent, 'm.room.canonical_alias': textForCanonicalAliasEvent, 'm.room.name': textForRoomNameEvent, 'm.room.topic': textForTopicEvent, diff --git a/src/WidgetMessaging.js b/src/WidgetMessaging.js index d40a8ab637..30c2389b1e 100644 --- a/src/WidgetMessaging.js +++ b/src/WidgetMessaging.js @@ -27,6 +27,7 @@ import {MatrixClientPeg} from "./MatrixClientPeg"; import SettingsStore from "./settings/SettingsStore"; import WidgetOpenIDPermissionsDialog from "./components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetUtils from "./utils/WidgetUtils"; +import {KnownWidgetActions} from "./widgets/WidgetApi"; if (!global.mxFromWidgetMessaging) { global.mxFromWidgetMessaging = new FromWidgetPostMessageApi(); @@ -75,6 +76,17 @@ export default class WidgetMessaging { }); } + /** + * Tells the widget that the client is ready to handle further widget requests. + * @returns {Promise<*>} Resolves after the widget has acknowledged the ready message. + */ + flagReadyToContinue() { + return this.messageToWidget({ + api: OUTBOUND_API_NAME, + action: KnownWidgetActions.ClientReady, + }); + } + /** * Request a screenshot from a widget * @return {Promise} To be resolved with screenshot data when it has been generated diff --git a/src/accessibility/KeyboardShortcuts.tsx b/src/accessibility/KeyboardShortcuts.tsx new file mode 100644 index 0000000000..c2739beefa --- /dev/null +++ b/src/accessibility/KeyboardShortcuts.tsx @@ -0,0 +1,355 @@ +/* +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 * as React from "react"; +import classNames from "classnames"; + +import * as sdk from "../index"; +import Modal from "../Modal"; +import { _t, _td } from "../languageHandler"; +import {isMac, Key} from "../Keyboard"; + +// TS: once languageHandler is TS we can probably inline this into the enum +_td("Navigation"); +_td("Calls"); +_td("Composer"); +_td("Room List"); +_td("Autocomplete"); + +export enum Categories { + NAVIGATION = "Navigation", + CALLS = "Calls", + COMPOSER = "Composer", + ROOM_LIST = "Room List", + AUTOCOMPLETE = "Autocomplete", +} + +// TS: once languageHandler is TS we can probably inline this into the enum +_td("Alt"); +_td("Alt Gr"); +_td("Shift"); +_td("Super"); +_td("Ctrl"); + +export enum Modifiers { + ALT = "Alt", // Option on Mac and displayed as an Icon + ALT_GR = "Alt Gr", + SHIFT = "Shift", + SUPER = "Super", // should this be "Windows"? + // Instead of using below, consider CMD_OR_CTRL + COMMAND = "Command", // This gets displayed as an Icon + CONTROL = "Ctrl", +} + +// Meta-modifier: isMac ? CMD : CONTROL +export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL; + +interface IKeybind { + modifiers?: Modifiers[]; + key: string; // TS: fix this once Key is an enum +} + +interface IShortcut { + keybinds: IKeybind[]; + description: string; +} + +const shortcuts: Record = { + [Categories.COMPOSER]: [ + { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.B, + }], + description: _td("Toggle Bold"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.I, + }], + description: _td("Toggle Italics"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.GREATER_THAN, + }], + description: _td("Toggle Quote"), + }, { + keybinds: [{ + modifiers: [Modifiers.SHIFT], + key: Key.ENTER, + }], + description: _td("New line"), + }, { + keybinds: [{ + key: Key.ARROW_UP, + }, { + key: Key.ARROW_DOWN, + }], + description: _td("Navigate recent messages to edit"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.HOME, + }, { + modifiers: [CMD_OR_CTRL], + key: Key.END, + }], + description: _td("Jump to start/end of the composer"), + }, { + keybinds: [{ + modifiers: [Modifiers.CONTROL, Modifiers.ALT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.CONTROL, Modifiers.ALT], + key: Key.ARROW_DOWN, + }], + description: _td("Navigate composer history"), + }, + ], + + [Categories.CALLS]: [ + { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.D, + }], + description: _td("Toggle microphone mute"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.E, + }], + description: _td("Toggle video on/off"), + }, + ], + + [Categories.ROOM_LIST]: [ + { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.K, + }], + description: _td("Jump to room search"), + }, { + keybinds: [{ + key: Key.ARROW_UP, + }, { + key: Key.ARROW_DOWN, + }], + description: _td("Navigate up/down in the room list"), + }, { + keybinds: [{ + key: Key.ENTER, + }], + description: _td("Select room from the room list"), + }, { + keybinds: [{ + key: Key.ARROW_LEFT, + }], + description: _td("Collapse room list section"), + }, { + keybinds: [{ + key: Key.ARROW_RIGHT, + }], + description: _td("Expand room list section"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Clear room list filter field"), + }, + ], + + [Categories.NAVIGATION]: [ + { + keybinds: [{ + key: Key.PAGE_UP, + }, { + key: Key.PAGE_DOWN, + }], + description: _td("Scroll up/down in the timeline"), + }, { + keybinds: [{ + modifiers: [Modifiers.ALT, Modifiers.SHIFT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.ALT, Modifiers.SHIFT], + key: Key.ARROW_DOWN, + }], + description: _td("Previous/next unread room or DM"), + }, { + keybinds: [{ + modifiers: [Modifiers.ALT], + key: Key.ARROW_UP, + }, { + modifiers: [Modifiers.ALT], + key: Key.ARROW_DOWN, + }], + description: _td("Previous/next room or DM"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.BACKTICK, + }], + description: _td("Toggle the top left menu"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Close dialog or context menu"), + }, { + keybinds: [{ + key: Key.ENTER, + }, { + key: Key.SPACE, + }], + description: _td("Activate selected button"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.PERIOD, + }], + description: _td("Toggle right panel"), + }, { + keybinds: [{ + modifiers: [CMD_OR_CTRL], + key: Key.SLASH, + }], + description: _td("Toggle this dialog"), + }, + ], + + [Categories.AUTOCOMPLETE]: [ + { + keybinds: [{ + key: Key.ARROW_UP, + }, { + key: Key.ARROW_DOWN, + }], + description: _td("Move autocomplete selection up/down"), + }, { + keybinds: [{ + key: Key.ESCAPE, + }], + description: _td("Cancel autocomplete"), + }, + ], +}; + +const categoryOrder = [ + Categories.COMPOSER, + Categories.CALLS, + Categories.ROOM_LIST, + Categories.AUTOCOMPLETE, + Categories.NAVIGATION, +]; + +interface IModal { + close: () => void; + finished: Promise; +} + +const modifierIcon: Record = { + [Modifiers.COMMAND]: "⌘", +}; + +if (isMac) { + modifierIcon[Modifiers.ALT] = "⌥"; +} + +const alternateKeyName: Record = { + [Key.PAGE_UP]: _td("Page Up"), + [Key.PAGE_DOWN]: _td("Page Down"), + [Key.ESCAPE]: _td("Esc"), + [Key.ENTER]: _td("Enter"), + [Key.SPACE]: _td("Space"), + [Key.HOME]: _td("Home"), + [Key.END]: _td("End"), +}; +const keyIcon: Record = { + [Key.ARROW_UP]: "↑", + [Key.ARROW_DOWN]: "↓", + [Key.ARROW_LEFT]: "←", + [Key.ARROW_RIGHT]: "→", +}; + +const Shortcut: React.FC<{ + shortcut: IShortcut; +}> = ({shortcut}) => { + const classes = classNames({ + "mx_KeyboardShortcutsDialog_inline": shortcut.keybinds.every(k => !k.modifiers || k.modifiers.length === 0), + }); + + return
+
{ _t(shortcut.description) }
+ { shortcut.keybinds.map(s => { + let text = s.key; + if (alternateKeyName[s.key]) { + text = _t(alternateKeyName[s.key]); + } else if (keyIcon[s.key]) { + text = keyIcon[s.key]; + } + + return
+ { s.modifiers && s.modifiers.map(m => { + return + { modifierIcon[m] || _t(m) }+ + ; + }) } + { text } +
; + }) } +
; +}; + +let activeModal: IModal = null; +export const toggleDialog = () => { + if (activeModal) { + activeModal.close(); + activeModal = null; + return; + } + + const sections = categoryOrder.map(category => { + const list = shortcuts[category]; + return
+

{_t(category)}

+
{list.map(shortcut => )}
+
; + }); + + const InfoDialog = sdk.getComponent('dialogs.InfoDialog'); + activeModal = Modal.createTrackedDialog("Keyboard Shortcuts", "", InfoDialog, { + className: "mx_KeyboardShortcutsDialog", + title: _t("Keyboard Shortcuts"), + description: sections, + hasCloseButton: true, + onKeyDown: (ev) => { + if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey && ev.key === Key.SLASH) { // Ctrl + / + ev.stopPropagation(); + activeModal.close(); + } + }, + onFinished: () => { + activeModal = null; + }, + }); +}; + +export const registerShortcut = (category: Categories, defn: IShortcut) => { + shortcuts[category].push(defn); +}; diff --git a/src/actions/RoomListActions.js b/src/actions/RoomListActions.js index d534fe5d1d..10a3848dda 100644 --- a/src/actions/RoomListActions.js +++ b/src/actions/RoomListActions.js @@ -15,7 +15,7 @@ limitations under the License. */ import { asyncAction } from './actionCreators'; -import RoomListStore from '../stores/RoomListStore'; +import RoomListStore, {TAG_DM} from '../stores/RoomListStore'; import Modal from '../Modal'; import * as Rooms from '../Rooms'; import { _t } from '../languageHandler'; @@ -73,11 +73,11 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const roomId = room.roomId; // Evil hack to get DMs behaving - if ((oldTag === undefined && newTag === 'im.vector.fake.direct') || - (oldTag === 'im.vector.fake.direct' && newTag === undefined) + if ((oldTag === undefined && newTag === TAG_DM) || + (oldTag === TAG_DM && newTag === undefined) ) { return Rooms.guessAndSetDMRoom( - room, newTag === 'im.vector.fake.direct', + room, newTag === TAG_DM, ).catch((err) => { const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); console.error("Failed to set direct chat tag " + err); @@ -91,10 +91,10 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, const hasChangedSubLists = oldTag !== newTag; // More evilness: We will still be dealing with moving to favourites/low prio, - // but we avoid ever doing a request with 'im.vector.fake.direct`. + // but we avoid ever doing a request with TAG_DM. // // if we moved lists, remove the old tag - if (oldTag && oldTag !== 'im.vector.fake.direct' && + if (oldTag && oldTag !== TAG_DM && hasChangedSubLists ) { const promiseToDelete = matrixClient.deleteRoomTag( @@ -112,7 +112,7 @@ RoomListActions.tagRoom = function(matrixClient, room, oldTag, newTag, oldIndex, } // if we moved lists or the ordering changed, add the new tag - if (newTag && newTag !== 'im.vector.fake.direct' && + if (newTag && newTag !== TAG_DM && (hasChangedSubLists || metaData) ) { // metaData is the body of the PUT to set the tag, so it must diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js index f3ea3beb1c..371fdcaf64 100644 --- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js +++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js @@ -141,15 +141,17 @@ export default class ManageEventIndexDialog extends React.Component { let crawlerState; if (this.state.currentRoom === null) { - crawlerState = _t("Not currently downloading messages for any room."); + crawlerState = _t("Not currently indexing messages for any room."); } else { crawlerState = ( - _t("Downloading mesages for %(currentRoom)s.", { currentRoom: this.state.currentRoom }) + _t("Currently indexing: %(currentRoom)s.", { currentRoom: this.state.currentRoom }) ); } const Field = sdk.getComponent('views.elements.Field'); + const doneRooms = Math.max(0, (this.state.roomCount - this.state.crawlingRoomsCount)); + const eventIndexingSettings = (
{ @@ -158,13 +160,13 @@ export default class ManageEventIndexDialog extends React.Component { ) }
+ {crawlerState}
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}
- {_t("Indexed rooms:")} {_t("%(crawlingRooms)s out of %(totalRooms)s", { - crawlingRooms: formatCountLong(this.state.crawlingRoomsCount), + {_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", { + doneRooms: formatCountLong(doneRooms), totalRooms: formatCountLong(this.state.roomCount), })}
- {crawlerState}
this._keyInfo, keyBackupInfo: this.state.backupInfo, setupNewKeyBackup: !this.state.backupInfo && this.state.useKeyBackup, + getKeyBackupPassphrase: promptForBackupPassphrase, }); } this.setState({ @@ -269,13 +276,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent { const RestoreKeyBackupDialog = sdk.getComponent('dialogs.keybackup.RestoreKeyBackupDialog'); const { finished } = Modal.createTrackedDialog( 'Restore Backup', '', RestoreKeyBackupDialog, {showSummary: false}, null, - /* priority = */ false, /* static = */ true, + /* priority = */ false, /* static = */ false, ); await finished; - await this._fetchBackupInfo(); + const { backupSigStatus } = await this._fetchBackupInfo(); if ( - this.state.backupSigStatus.usable && + backupSigStatus.usable && this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword ) { @@ -400,12 +407,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent { let authPrompt; let nextCaption = _t("Next"); - if (!this.state.backupSigStatus.usable) { - authPrompt =
-
{_t("Restore your key backup to upgrade your encryption")}
-
; - nextCaption = _t("Restore"); - } else if (this.state.canUploadKeysWithPasswordOnly) { + if (this.state.canUploadKeysWithPasswordOnly) { authPrompt =
{_t("Enter your account password to confirm the upgrade:")}
; + } else if (!this.state.backupSigStatus.usable) { + authPrompt =
+
{_t("Restore your key backup to upgrade your encryption")}
+
; + nextCaption = _t("Restore"); } else { authPrompt =

{_t("You'll need to authenticate with the server to confirm the upgrade.")} @@ -433,6 +440,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {

{authPrompt}
diff --git a/src/components/structures/ContextMenu.js b/src/components/structures/ContextMenu.js index 898991f4f2..b4647a6c30 100644 --- a/src/components/structures/ContextMenu.js +++ b/src/components/structures/ContextMenu.js @@ -350,7 +350,7 @@ export const ContextMenuButton = ({ label, isExpanded, children, ...props }) => }; ContextMenuButton.propTypes = { ...AccessibleButton.propTypes, - label: PropTypes.string.isRequired, + label: PropTypes.string, isExpanded: PropTypes.bool.isRequired, // whether or not the context menu is currently open }; @@ -377,7 +377,6 @@ export const MenuGroup = ({children, label, ...props}) => {
; }; MenuGroup.propTypes = { - ...AccessibleButton.propTypes, label: PropTypes.string.isRequired, className: PropTypes.string, // optional }; diff --git a/src/components/structures/EmbeddedPage.js b/src/components/structures/EmbeddedPage.js index 6d734c3838..f854dc955f 100644 --- a/src/components/structures/EmbeddedPage.js +++ b/src/components/structures/EmbeddedPage.js @@ -23,11 +23,11 @@ import PropTypes from 'prop-types'; import request from 'browser-request'; import { _t } from '../../languageHandler'; import sanitizeHtml from 'sanitize-html'; -import * as sdk from '../../index'; import dis from '../../dispatcher'; import {MatrixClientPeg} from '../../MatrixClientPeg'; import classnames from 'classnames'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import AutoHideScrollbar from "./AutoHideScrollbar"; export default class EmbeddedPage extends React.PureComponent { static propTypes = { @@ -117,10 +117,9 @@ export default class EmbeddedPage extends React.PureComponent {
; if (this.props.scrollbar) { - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - return + return {content} - ; + ; } else { return
{content} diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js index e98dcae1a4..524694fe95 100644 --- a/src/components/structures/GroupView.js +++ b/src/components/structures/GroupView.js @@ -39,6 +39,7 @@ import {makeGroupPermalink, makeUserPermalink} from "../../utils/permalinks/Perm import {Group} from "matrix-js-sdk"; import {allSettled, sleep} from "../../utils/promise"; import RightPanelStore from "../../stores/RightPanelStore"; +import AutoHideScrollbar from "./AutoHideScrollbar"; const LONG_DESC_PLACEHOLDER = _td( `

HTML for your community's page

@@ -423,6 +424,7 @@ export default createReactClass({ membershipBusy: false, publicityBusy: false, inviterProfile: null, + showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, }; }, @@ -435,12 +437,18 @@ export default createReactClass({ this._initGroupStore(this.props.groupId, true); this._dispatcherRef = dis.register(this._onAction); + this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); }, componentWillUnmount: function() { this._unmounted = true; this._matrixClient.removeListener("Group.myMembership", this._onGroupMyMembership); dis.unregister(this._dispatcherRef); + + // Remove RightPanelStore listener + if (this._rightPanelStoreToken) { + this._rightPanelStoreToken.remove(); + } }, componentWillReceiveProps: function(newProps) { @@ -454,6 +462,12 @@ export default createReactClass({ } }, + _onRightPanelStoreUpdate: function() { + this.setState({ + showRightPanel: RightPanelStore.getSharedInstance().isOpenForGroup, + }); + }, + _onGroupMyMembership: function(group) { if (this._unmounted || group.groupId !== this.props.groupId) return; if (group.myMembership === 'leave') { @@ -481,7 +495,7 @@ export default createReactClass({ group_id: groupId, }, }); - dis.dispatch({action: 'require_registration'}); + dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${groupId}`}}); willDoOnboarding = true; } if (stateKey === GroupStore.STATE_KEY.Summary) { @@ -554,10 +568,6 @@ export default createReactClass({ GROUP_JOINPOLICY_INVITE, }, }); - dis.dispatch({ - action: 'panel_disable', - sideDisabled: true, - }); }, _onShareClick: function() { @@ -580,10 +590,6 @@ export default createReactClass({ profileForm: null, }); break; - case 'after_right_panel_phase_change': - // We don't keep state on the right panel, so just re-render to update - this.forceUpdate(); - break; default: break; } @@ -726,7 +732,7 @@ export default createReactClass({ _onJoinClick: async function() { if (this._matrixClient.isGuest()) { - dis.dispatch({action: 'require_registration'}); + dis.dispatch({action: 'require_registration', screen_after: {screen: `group/${this.props.groupId}`}}); return; } @@ -1173,7 +1179,6 @@ export default createReactClass({ render: function() { const GroupAvatar = sdk.getComponent("avatars.GroupAvatar"); const Spinner = sdk.getComponent("elements.Spinner"); - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); if (this.state.summaryLoading && this.state.error === null || this.state.saving) { return ; @@ -1299,9 +1304,7 @@ export default createReactClass({ ); } - const rightPanel = RightPanelStore.getSharedInstance().isOpenForGroup - ? - : undefined; + const rightPanel = this.state.showRightPanel ? : undefined; const headerClasses = { "mx_GroupView_header": true, @@ -1332,10 +1335,10 @@ export default createReactClass({
- + { this._getMembershipSection() } { this._getGroupSection() } - + ); diff --git a/src/components/structures/InteractiveAuth.js b/src/components/structures/InteractiveAuth.js index 3d63029b06..f4adb5751f 100644 --- a/src/components/structures/InteractiveAuth.js +++ b/src/components/structures/InteractiveAuth.js @@ -161,6 +161,7 @@ export default createReactClass({ _authStateUpdated: function(stageType, stageState) { const oldStage = this.state.authStage; this.setState({ + busy: false, authStage: stageType, stageState: stageState, errorText: stageState.error, @@ -184,11 +185,13 @@ export default createReactClass({ errorText: null, stageErrorText: null, }); - } else { - this.setState({ - busy: false, - }); } + // The JS SDK eagerly reports itself as "not busy" right after any + // immediate work has completed, but that's not really what we want at + // the UI layer, so we ignore this signal and show a spinner until + // there's a new screen to show the user. This is implemented by setting + // `busy: false` in `_authStateUpdated`. + // See also https://github.com/vector-im/riot-web/issues/12546 }, _setFocus: function() { diff --git a/src/components/structures/LoggedInView.js b/src/components/structures/LoggedInView.js index 9597f99cd2..e7a6f4c1a9 100644 --- a/src/components/structures/LoggedInView.js +++ b/src/components/structures/LoggedInView.js @@ -39,6 +39,7 @@ import RoomListActions from '../../actions/RoomListActions'; import ResizeHandle from '../views/elements/ResizeHandle'; import {Resizer, CollapseDistributor} from '../../resizer'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import * as KeyboardShortcuts from "../../accessibility/KeyboardShortcuts"; // We need to fetch each pinned message individually (if we don't already have it) // so each pinned message may trigger a request. Limit the number per room for sanity. // NB. this is just for server notices rather than pinned messages in general. @@ -337,13 +338,13 @@ const LoggedInView = createReactClass({ let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); - const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey || - ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT; + const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey; + const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT; switch (ev.key) { case Key.PAGE_UP: case Key.PAGE_DOWN: - if (!hasModifier) { + if (!hasModifier && !isModifier) { this._onScrollKeyPressed(ev); handled = true; } @@ -365,8 +366,6 @@ const LoggedInView = createReactClass({ } break; case Key.BACKTICK: - if (ev.key !== "`") break; - // Ideally this would be CTRL+P for "Profile", but that's // taken by the print dialog. CTRL+I for "Information" // was previously chosen but conflicted with italics in @@ -379,12 +378,43 @@ const LoggedInView = createReactClass({ handled = true; } break; + + case Key.SLASH: + if (ctrlCmdOnly) { + KeyboardShortcuts.toggleDialog(); + handled = true; + } + break; + + case Key.ARROW_UP: + case Key.ARROW_DOWN: + if (ev.altKey && !ev.ctrlKey && !ev.metaKey) { + dis.dispatch({ + action: 'view_room_delta', + delta: ev.key === Key.ARROW_UP ? -1 : 1, + unread: ev.shiftKey, + }); + handled = true; + } + break; + + case Key.PERIOD: + if (ctrlCmdOnly && (this.props.page_type === "room_view" || this.props.page_type === "group_view")) { + dis.dispatch({ + action: 'toggle_right_panel', + type: this.props.page_type === "room_view" ? "room" : "group", + }); + handled = true; + } } if (handled) { ev.stopPropagation(); ev.preventDefault(); - } else if (!hasModifier) { + } else if (!isModifier && !ev.altKey && !ev.ctrlKey && !ev.metaKey) { + // The above condition is crafted to _allow_ characters with Shift + // already pressed (but not the Shift key down itself). + const isClickShortcut = ev.target !== document.body && (ev.key === Key.SPACE || ev.key === Key.ENTER); @@ -585,7 +615,8 @@ const LoggedInView = createReactClass({ limitType={usageLimitEvent.getContent().limit_type} />; } else if (this.props.showCookieBar && - this.props.config.piwik + this.props.config.piwik && + navigator.doNotTrack !== "1" ) { const policyUrl = this.props.config.piwik.policyUrl || null; topBar = ; diff --git a/src/components/structures/MainSplit.js b/src/components/structures/MainSplit.js index 772be358cf..7c66f21a04 100644 --- a/src/components/structures/MainSplit.js +++ b/src/components/structures/MainSplit.js @@ -93,14 +93,19 @@ export default class MainSplit extends React.Component { const bodyView = React.Children.only(this.props.children); const panelView = this.props.panel; - if (this.props.collapsedRhs || !panelView) { - return bodyView; - } else { - return
- { bodyView } + const hasResizer = !this.props.collapsedRhs && panelView; + + let children; + if (hasResizer) { + children = { panelView } -
; + ; } + + return
+ { bodyView } + { children } +
; } } diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js index 339ea279ee..52002f0591 100644 --- a/src/components/structures/MatrixChat.js +++ b/src/components/structures/MatrixChat.js @@ -559,13 +559,19 @@ export default createReactClass({ case 'view_user_info': this._viewUser(payload.userId, payload.subAction); break; - case 'view_room': + case 'view_room': { // Takes either a room ID or room alias: if switching to a room the client is already // known to be in (eg. user clicks on a room in the recents panel), supply the ID // If the user is clicking on a room in the context of the alias being presented // to them, supply the room alias. If both are supplied, the room ID will be ignored. - this._viewRoom(payload); + const promise = this._viewRoom(payload); + if (payload.deferred_action) { + promise.then(() => { + dis.dispatch(payload.deferred_action); + }); + } break; + } case 'view_prev_room': this._viewNextRoom(-1); break; @@ -594,9 +600,8 @@ export default createReactClass({ break; case 'view_room_directory': { const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); - Modal.createTrackedDialog('Room directory', '', RoomDirectory, { - config: this.props.config, - }, 'mx_RoomDirectory_dialogWrapper'); + Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, + 'mx_RoomDirectory_dialogWrapper', false, true); // View the welcome or home page if we need something to look at this._viewSomethingBehindModal(); @@ -862,7 +867,7 @@ export default createReactClass({ waitFor = this.firstSyncPromise.promise; } - waitFor.then(() => { + return waitFor.then(() => { let presentedId = roomInfo.room_alias || roomInfo.room_id; const room = MatrixClientPeg.get().getRoom(roomInfo.room_id); if (room) { @@ -885,7 +890,7 @@ export default createReactClass({ presentedId += "/" + roomInfo.event_id; } newState.ready = true; - this.setState(newState, ()=>{ + this.setState(newState, () => { this.notifyNewScreen('room/' + presentedId); }); }); @@ -1008,6 +1013,10 @@ export default createReactClass({ // needs to be reset so that they can revisit /user/.. // (and trigger // `_chatCreateOrReuse` again) go_welcome_on_cancel: true, + screen_after: { + screen: `user/${this.props.config.welcomeUserId}`, + params: { action: 'chat' }, + }, }); return; } @@ -1177,7 +1186,15 @@ export default createReactClass({ _onLoggedIn: async function() { ThemeController.isLogin = false; this.setStateForNewView({ view: VIEWS.LOGGED_IN }); - if (MatrixClientPeg.currentUserIsJustRegistered()) { + // If a specific screen is set to be shown after login, show that above + // all else, as it probably means the user clicked on something already. + if (this._screenAfterLogin && this._screenAfterLogin.screen) { + this.showScreen( + this._screenAfterLogin.screen, + this._screenAfterLogin.params, + ); + this._screenAfterLogin = null; + } else if (MatrixClientPeg.currentUserIsJustRegistered()) { MatrixClientPeg.setJustRegisteredUserId(null); if (this.props.config.welcomeUserId && getCurrentLanguage().startsWith("en")) { @@ -1477,26 +1494,40 @@ export default createReactClass({ } }); - if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { - cli.on("crypto.verification.request", request => { - if (request.pending) { - ToastStore.sharedInstance().addOrReplaceToast({ - key: 'verifreq_' + request.channel.transactionId, - title: _t("Verification Request"), - icon: "verification", - props: {request}, - component: sdk.getComponent("toasts.VerificationRequestToast"), - }); - } - }); - } else { - cli.on("crypto.verification.start", (verifier) => { + cli.on("crypto.keySignatureUploadFailure", (failures, source, continuation) => { + const KeySignatureUploadFailedDialog = + sdk.getComponent('views.dialogs.KeySignatureUploadFailedDialog'); + Modal.createTrackedDialog( + 'Failed to upload key signatures', + 'Failed to upload key signatures', + KeySignatureUploadFailedDialog, + { failures, source, continuation }); + }); + + cli.on("crypto.verification.request", request => { + const isFlagOn = SettingsStore.isFeatureEnabled("feature_cross_signing"); + + if (!isFlagOn && !request.channel.deviceId) { + request.cancel({code: "m.invalid_message", reason: "This client has cross-signing disabled"}); + return; + } + + if (request.verifier) { const IncomingSasDialog = sdk.getComponent("views.dialogs.IncomingSasDialog"); Modal.createTrackedDialog('Incoming Verification', '', IncomingSasDialog, { - verifier, + verifier: request.verifier, }, null, /* priority = */ false, /* static = */ true); - }); - } + } else if (request.pending) { + ToastStore.sharedInstance().addOrReplaceToast({ + key: 'verifreq_' + request.channel.transactionId, + title: _t("Verification Request"), + icon: "verification", + props: {request}, + component: sdk.getComponent("toasts.VerificationRequestToast"), + priority: ToastStore.PRIORITY_REALTIME, + }); + } + }); // Fire the tinter right on startup to ensure the default theme is applied // A later sync can/will correct the tint to be the right value for the user const colorScheme = SettingsStore.getValue("roomColor"); @@ -1890,7 +1921,10 @@ export default createReactClass({ // secret storage. SettingsStore.setFeatureEnabled("feature_cross_signing", true); this.setStateForNewView({ view: VIEWS.COMPLETE_SECURITY }); - } else if (SettingsStore.isFeatureEnabled("feature_cross_signing")) { + } else if ( + SettingsStore.isFeatureEnabled("feature_cross_signing") && + await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing") + ) { // This will only work if the feature is set to 'enable' in the config, // since it's too early in the lifecycle for users to have turned the // labs flag on. diff --git a/src/components/structures/MessagePanel.js b/src/components/structures/MessagePanel.js index d35b0fce1f..0ab6f1b107 100644 --- a/src/components/structures/MessagePanel.js +++ b/src/components/structures/MessagePanel.js @@ -28,6 +28,7 @@ import {MatrixClientPeg} from '../../MatrixClientPeg'; import SettingsStore from '../../settings/SettingsStore'; import {_t} from "../../languageHandler"; import {haveTileForEvent} from "../views/rooms/EventTile"; +import {textForEvent} from "../../TextForEvent"; const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const continuedTypes = ['m.sticker', 'm.room.message']; @@ -523,7 +524,8 @@ export default class MessagePanel extends React.Component { // if there is a previous event and it has the same sender as this event // and the types are the same/is in continuedTypes and the time between them is <= CONTINUATION_MAX_INTERVAL if (prevEvent !== null && prevEvent.sender && mxEv.sender && mxEv.sender.userId === prevEvent.sender.userId && - (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && + // if we don't have tile for previous event then it was shown by showHiddenEvents and has no SenderProfile + haveTileForEvent(prevEvent) && (mxEv.getType() === prevEvent.getType() || eventTypeContinues) && (mxEv.getTs() - prevEvent.getTs() <= CONTINUATION_MAX_INTERVAL)) { continuation = true; } @@ -879,6 +881,11 @@ class CreationGrouper { } getTiles() { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events || !this.events.length) return []; + const DateSeparator = sdk.getComponent('messages.DateSeparator'); const EventListSummary = sdk.getComponent('views.elements.EventListSummary'); @@ -956,15 +963,30 @@ class MemberGrouper { } shouldGroup(ev) { + if (this.panel._wantsDateSeparator(this.events[0], ev.getDate())) { + return false; + } return isMembershipChange(ev); } add(ev) { + if (ev.getType() === 'm.room.member') { + // We'll just double check that it's worth our time to do so, through an + // ugly hack. If textForEvent returns something, we should group it for + // rendering but if it doesn't then we'll exclude it. + const renderText = textForEvent(ev); + if (!renderText || renderText.trim().length === 0) return; // quietly ignore + } this.readMarker = this.readMarker || this.panel._readMarkerForEvent(ev.getId()); this.events.push(ev); } getTiles() { + // If we don't have any events to group, don't even try to group them. The logic + // below assumes that we have a group of events to deal with, but we might not if + // the events we were supposed to group were redacted. + if (!this.events || !this.events.length) return []; + const DateSeparator = sdk.getComponent('messages.DateSeparator'); const MemberEventListSummary = sdk.getComponent('views.elements.MemberEventListSummary'); diff --git a/src/components/structures/MyGroups.js b/src/components/structures/MyGroups.js index b26ab5ff70..f1209b7b9e 100644 --- a/src/components/structures/MyGroups.js +++ b/src/components/structures/MyGroups.js @@ -22,6 +22,7 @@ import { _t } from '../../languageHandler'; import dis from '../../dispatcher'; import AccessibleButton from '../views/elements/AccessibleButton'; import MatrixClientContext from "../../contexts/MatrixClientContext"; +import AutoHideScrollbar from "./AutoHideScrollbar"; export default createReactClass({ displayName: 'MyGroups', @@ -62,8 +63,6 @@ export default createReactClass({ const Loader = sdk.getComponent("elements.Spinner"); const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader'); const GroupTile = sdk.getComponent("groups.GroupTile"); - const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper"); - let content; let contentHeader; @@ -74,7 +73,7 @@ export default createReactClass({ }); contentHeader = groupNodes.length > 0 ?

{ _t('Your Communities') }

:
; content = groupNodes.length > 0 ? - +

{ _t( @@ -93,7 +92,7 @@ export default createReactClass({

{ groupNodes }
- : + :
{ _t( "You're not currently a member of any communities.", diff --git a/src/components/structures/RightPanel.js b/src/components/structures/RightPanel.js index 20df323c10..8d25116827 100644 --- a/src/components/structures/RightPanel.js +++ b/src/components/structures/RightPanel.js @@ -182,6 +182,7 @@ export default class RightPanel extends React.Component { member: payload.member, event: payload.event, verificationRequest: payload.verificationRequest, + verificationRequestPromise: payload.verificationRequestPromise, }); } } @@ -231,6 +232,7 @@ export default class RightPanel extends React.Component { onClose={onClose} phase={this.state.phase} verificationRequest={this.state.verificationRequest} + verificationRequestPromise={this.state.verificationRequestPromise} />; } else { panel = + -
; } const explanation = @@ -628,7 +638,7 @@ export default createReactClass({ title={_t("Explore rooms")} >
-

{explanation}

+ {explanation}
{listHeader} {content} diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js index 600b418fe0..9428de3e22 100644 --- a/src/components/structures/RoomSubList.js +++ b/src/components/structures/RoomSubList.js @@ -46,8 +46,6 @@ export default class RoomSubList extends React.PureComponent { tagName: PropTypes.string, addRoomLabel: PropTypes.string, - order: PropTypes.string.isRequired, - // passed through to RoomTile and used to highlight room with `!` regardless of notifications count isInvite: PropTypes.bool, @@ -113,21 +111,30 @@ export default class RoomSubList extends React.PureComponent { } onAction = (payload) => { - // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, - // but this is no longer true, so we must do it here (and can apply the small - // optimisation of checking that we care about the room being read). - // - // Ultimately we need to transition to a state pushing flow where something - // explicitly notifies the components concerned that the notif count for a room - // has change (e.g. a Flux store). - if (payload.action === 'on_room_read' && - this.props.list.some((r) => r.roomId === payload.roomId) - ) { - this.forceUpdate(); + switch (payload.action) { + case 'on_room_read': + // XXX: Previously RoomList would forceUpdate whenever on_room_read is dispatched, + // but this is no longer true, so we must do it here (and can apply the small + // optimisation of checking that we care about the room being read). + // + // Ultimately we need to transition to a state pushing flow where something + // explicitly notifies the components concerned that the notif count for a room + // has change (e.g. a Flux store). + if (this.props.list.some((r) => r.roomId === payload.roomId)) { + this.forceUpdate(); + } + break; + + case 'view_room': + if (this.state.hidden && !this.props.forceExpand && + this.props.list.some((r) => r.roomId === payload.room_id) + ) { + this.toggle(); + } } }; - onClick = (ev) => { + toggle = () => { if (this.isCollapsibleOnClick()) { // The header isCollapsible, so the click is to be interpreted as collapse and truncation logic const isHidden = !this.state.hidden; @@ -140,6 +147,10 @@ export default class RoomSubList extends React.PureComponent { } }; + onClick = (ev) => { + this.toggle(); + }; + onHeaderKeyDown = (ev) => { switch (ev.key) { case Key.ARROW_LEFT: diff --git a/src/components/structures/RoomView.js b/src/components/structures/RoomView.js index 0efa46416e..5fd5f42f78 100644 --- a/src/components/structures/RoomView.js +++ b/src/components/structures/RoomView.js @@ -30,7 +30,6 @@ import classNames from 'classnames'; import { _t } from '../../languageHandler'; import {RoomPermalinkCreator} from '../../utils/permalinks/Permalinks'; -import {MatrixClientPeg} from '../../MatrixClientPeg'; import ContentMessages from '../../ContentMessages'; import Modal from '../../Modal'; import * as sdk from '../../index'; @@ -55,6 +54,7 @@ import AccessibleButton from "../views/elements/AccessibleButton"; import RightPanelStore from "../../stores/RightPanelStore"; import {haveTileForEvent} from "../views/rooms/EventTile"; import RoomContext from "../../contexts/RoomContext"; +import MatrixClientContext from "../../contexts/MatrixClientContext"; const DEBUG = false; let debuglog = function() {}; @@ -97,8 +97,12 @@ export default createReactClass({ viaServers: PropTypes.arrayOf(PropTypes.string), }, + statics: { + contextType: MatrixClientContext, + }, + getInitialState: function() { - const llMembers = MatrixClientPeg.get().hasLazyLoadMembersEnabled(); + const llMembers = this.context.hasLazyLoadMembersEnabled(); return { room: null, roomId: null, @@ -131,6 +135,8 @@ export default createReactClass({ isAlone: false, isPeeking: false, showingPinned: false, + showReadReceipts: true, + showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, // error object, as from the matrix client/server API // If we failed to load information about the room, @@ -163,27 +169,36 @@ export default createReactClass({ componentWillMount: function() { this.dispatcherRef = dis.register(this.onAction); - MatrixClientPeg.get().on("Room", this.onRoom); - MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().on("Room.name", this.onRoomName); - MatrixClientPeg.get().on("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); - MatrixClientPeg.get().on("RoomState.members", this.onRoomStateMember); - MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); - MatrixClientPeg.get().on("accountData", this.onAccountData); - MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus); - MatrixClientPeg.get().on("deviceVerificationChanged", this.onDeviceVerificationChanged); - MatrixClientPeg.get().on("userTrustStatusChanged", this.onUserVerificationChanged); + this.context.on("Room", this.onRoom); + this.context.on("Room.timeline", this.onRoomTimeline); + this.context.on("Room.name", this.onRoomName); + this.context.on("Room.accountData", this.onRoomAccountData); + this.context.on("RoomState.events", this.onRoomStateEvents); + this.context.on("RoomState.members", this.onRoomStateMember); + this.context.on("Room.myMembership", this.onMyMembership); + this.context.on("accountData", this.onAccountData); + this.context.on("crypto.keyBackupStatus", this.onKeyBackupStatus); + this.context.on("deviceVerificationChanged", this.onDeviceVerificationChanged); + this.context.on("userTrustStatusChanged", this.onUserVerificationChanged); // Start listening for RoomViewStore updates this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate); + this._rightPanelStoreToken = RightPanelStore.getSharedInstance().addListener(this._onRightPanelStoreUpdate); this._onRoomViewStoreUpdate(true); WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate); + this._showReadReceiptsWatchRef = SettingsStore.watchSetting("showReadReceipts", null, + this._onReadReceiptsChange); this._roomView = createRef(); this._searchResultsPanel = createRef(); }, + _onReadReceiptsChange: function() { + this.setState({ + showReadReceipts: SettingsStore.getValue("showReadReceipts", this.state.roomId), + }); + }, + _onRoomViewStoreUpdate: function(initial) { if (this.unmounted) { return; @@ -204,8 +219,10 @@ export default createReactClass({ return; } + const roomId = RoomViewStore.getRoomId(); + const newState = { - roomId: RoomViewStore.getRoomId(), + roomId, roomAlias: RoomViewStore.getRoomAlias(), roomLoading: RoomViewStore.isRoomLoading(), roomLoadError: RoomViewStore.getRoomLoadError(), @@ -214,7 +231,8 @@ export default createReactClass({ isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), forwardingEvent: RoomViewStore.getForwardingEvent(), shouldPeek: RoomViewStore.shouldPeek(), - showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()), + showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", roomId), + showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), }; // Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307 @@ -231,7 +249,7 @@ export default createReactClass({ // NB: This does assume that the roomID will not change for the lifetime of // the RoomView instance if (initial) { - newState.room = MatrixClientPeg.get().getRoom(newState.roomId); + newState.room = this.context.getRoom(newState.roomId); if (newState.room) { newState.showApps = this._shouldShowApps(newState.room); this._onRoomLoaded(newState.room); @@ -333,7 +351,7 @@ export default createReactClass({ peekLoading: true, isPeeking: true, // this will change to false if peeking fails }); - MatrixClientPeg.get().peekInRoom(roomId).then((room) => { + this.context.peekInRoom(roomId).then((room) => { if (this.unmounted) { return; } @@ -342,7 +360,7 @@ export default createReactClass({ peekLoading: false, }); this._onRoomLoaded(room); - }, (err) => { + }).catch((err) => { if (this.unmounted) { return; } @@ -355,7 +373,7 @@ export default createReactClass({ // This won't necessarily be a MatrixError, but we duck-type // here and say if it's got an 'errcode' key with the right value, // it means we can't peek. - if (err.errcode == "M_GUEST_ACCESS_FORBIDDEN") { + if (err.errcode === "M_GUEST_ACCESS_FORBIDDEN" || err.errcode === 'M_FORBIDDEN') { // This is fine: the room just isn't peekable (we assume). this.setState({ peekLoading: false, @@ -365,10 +383,8 @@ export default createReactClass({ } }); } else if (room) { - //viewing a previously joined room, try to lazy load members - // Stop peeking because we have joined this room previously - MatrixClientPeg.get().stopPeeking(); + this.context.stopPeeking(); this.setState({isPeeking: false}); } } @@ -407,21 +423,6 @@ export default createReactClass({ this.onResize(); document.addEventListener("keydown", this.onKeyDown); - - // XXX: EVIL HACK to autofocus inviting on empty rooms. - // We use the setTimeout to avoid racing with focus_composer. - if (this.state.room && - this.state.room.getJoinedMemberCount() == 1 && - this.state.room.getLiveTimeline() && - this.state.room.getLiveTimeline().getEvents() && - this.state.room.getLiveTimeline().getEvents().length <= 6) { - const inviteBox = document.getElementById("mx_SearchableEntityList_query"); - setTimeout(function() { - if (inviteBox) { - inviteBox.focus(); - } - }, 50); - } }, shouldComponentUpdate: function(nextProps, nextState) { @@ -480,18 +481,18 @@ export default createReactClass({ roomView.removeEventListener('dragend', this.onDragLeaveOrEnd); } dis.unregister(this.dispatcherRef); - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener("Room", this.onRoom); - MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline); - MatrixClientPeg.get().removeListener("Room.name", this.onRoomName); - MatrixClientPeg.get().removeListener("Room.accountData", this.onRoomAccountData); - MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); - MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); - MatrixClientPeg.get().removeListener("RoomState.members", this.onRoomStateMember); - MatrixClientPeg.get().removeListener("accountData", this.onAccountData); - MatrixClientPeg.get().removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); - MatrixClientPeg.get().removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); - MatrixClientPeg.get().removeListener("userTrustStatusChanged", this.onUserVerificationChanged); + if (this.context) { + this.context.removeListener("Room", this.onRoom); + this.context.removeListener("Room.timeline", this.onRoomTimeline); + this.context.removeListener("Room.name", this.onRoomName); + this.context.removeListener("Room.accountData", this.onRoomAccountData); + this.context.removeListener("RoomState.events", this.onRoomStateEvents); + this.context.removeListener("Room.myMembership", this.onMyMembership); + this.context.removeListener("RoomState.members", this.onRoomStateMember); + this.context.removeListener("accountData", this.onAccountData); + this.context.removeListener("crypto.keyBackupStatus", this.onKeyBackupStatus); + this.context.removeListener("deviceVerificationChanged", this.onDeviceVerificationChanged); + this.context.removeListener("userTrustStatusChanged", this.onUserVerificationChanged); } window.removeEventListener('beforeunload', this.onPageUnload); @@ -505,9 +506,18 @@ export default createReactClass({ if (this._roomStoreToken) { this._roomStoreToken.remove(); } + // Remove RightPanelStore listener + if (this._rightPanelStoreToken) { + this._rightPanelStoreToken.remove(); + } WidgetEchoStore.removeListener('update', this._onWidgetEchoStoreUpdate); + if (this._showReadReceiptsWatchRef) { + SettingsStore.unwatchSetting(this._showReadReceiptsWatchRef); + this._showReadReceiptsWatchRef = null; + } + // cancel any pending calls to the rate_limited_funcs this._updateRoomMembers.cancelPendingCall(); @@ -516,6 +526,12 @@ export default createReactClass({ // Tinter.tint(); // reset colourscheme }, + _onRightPanelStoreUpdate: function() { + this.setState({ + showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, + }); + }, + onPageUnload(event) { if (ContentMessages.sharedInstance().getCurrentUploads().length > 0) { return event.returnValue = @@ -526,7 +542,6 @@ export default createReactClass({ } }, - onKeyDown: function(ev) { let handled = false; const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); @@ -555,10 +570,6 @@ export default createReactClass({ onAction: function(payload) { switch (payload.action) { - case 'after_right_panel_phase_change': - // We don't keep state on the right panel, so just re-render to update - this.forceUpdate(); - break; case 'message_send_failed': case 'message_sent': this._checkIfAlone(this.state.room); @@ -570,9 +581,7 @@ export default createReactClass({ payload.data.description || payload.data.name); break; case 'picture_snapshot': - ContentMessages.sharedInstance().sendContentListToRoom( - [payload.file], this.state.room.roomId, MatrixClientPeg.get(), - ); + ContentMessages.sharedInstance().sendContentListToRoom([payload.file], this.state.room.roomId, this.context); break; case 'notifier_enabled': case 'upload_started': @@ -616,6 +625,22 @@ export default createReactClass({ this.onCancelSearchClick(); } break; + case 'quote': + if (this.state.searchResults) { + const roomId = payload.event.getRoomId(); + if (roomId === this.state.roomId) { + this.onCancelSearchClick(); + } + + setImmediate(() => { + dis.dispatch({ + action: 'view_room', + room_id: roomId, + deferred_action: payload, + }); + }); + } + break; } }, @@ -645,7 +670,7 @@ export default createReactClass({ // we'll only be showing a spinner. if (this.state.joining) return; - if (ev.getSender() !== MatrixClientPeg.get().credentials.userId) { + if (ev.getSender() !== this.context.credentials.userId) { // update unread count when scrolled up if (!this.state.searchResults && this.state.atEndOfLiveTimeline) { // no change @@ -701,8 +726,7 @@ export default createReactClass({ _loadMembersIfJoined: async function(room) { // lazy load members if enabled - const cli = MatrixClientPeg.get(); - if (cli.hasLazyLoadMembersEnabled()) { + if (this.context.hasLazyLoadMembersEnabled()) { if (room && room.getMyMembership() === 'join') { try { await room.loadMembersIfNeeded(); @@ -737,7 +761,7 @@ export default createReactClass({ _updatePreviewUrlVisibility: function({roomId}) { // URL Previews in E2EE rooms can be a privacy leak so use a different setting which is per-room explicit - const key = MatrixClientPeg.get().isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'; + const key = this.context.isRoomEncrypted(roomId) ? 'urlPreviewsEnabled_e2ee' : 'urlPreviewsEnabled'; this.setState({ showUrlPreview: SettingsStore.getValue(key, roomId), }); @@ -771,11 +795,10 @@ export default createReactClass({ }, _updateE2EStatus: async function(room) { - const cli = MatrixClientPeg.get(); - if (!cli.isRoomEncrypted(room.roomId)) { + if (!this.context.isRoomEncrypted(room.roomId)) { return; } - if (!cli.isCryptoEnabled()) { + if (!this.context.isCryptoEnabled()) { // If crypto is not currently enabled, we aren't tracking devices at all, // so we don't know what the answer is. Let's error on the safe side and show // a warning for this case. @@ -800,21 +823,21 @@ export default createReactClass({ const verified = []; const unverified = []; e2eMembers.map(({userId}) => userId) - .filter((userId) => userId !== cli.getUserId()) + .filter((userId) => userId !== this.context.getUserId()) .forEach((userId) => { - (cli.checkUserTrust(userId).isCrossSigningVerified() ? - verified : unverified).push(userId) + (this.context.checkUserTrust(userId).isCrossSigningVerified() ? + verified : unverified).push(userId); }); debuglog("e2e verified", verified, "unverified", unverified); /* Check all verified user devices. */ /* Don't alarm if no other users are verified */ - const targets = (verified.length > 0) ? [...verified, cli.getUserId()] : verified; + const targets = (verified.length > 0) ? [...verified, this.context.getUserId()] : verified; for (const userId of targets) { - const devices = await cli.getStoredDevicesForUser(userId); + const devices = await this.context.getStoredDevicesForUser(userId); const anyDeviceNotVerified = devices.some(({deviceId}) => { - return !cli.checkDeviceTrust(userId, deviceId).isVerified(); + return !this.context.checkDeviceTrust(userId, deviceId).isVerified(); }); if (anyDeviceNotVerified) { this.setState({ @@ -896,7 +919,7 @@ export default createReactClass({ _updatePermissions: function(room) { if (room) { - const me = MatrixClientPeg.get().getUserId(); + const me = this.context.getUserId(); const canReact = room.getMyMembership() === "join" && room.currentState.maySendEvent("m.reaction", me); const canReply = room.maySendMessage(); @@ -980,7 +1003,7 @@ export default createReactClass({ if (this.state.searchResults.next_batch) { debuglog("requesting more search results"); - const searchPromise = MatrixClientPeg.get().backPaginateRoomEventsSearch( + const searchPromise = this.context.backPaginateRoomEventsSearch( this.state.searchResults); return this._handleSearchResult(searchPromise); } else { @@ -1006,10 +1029,8 @@ export default createReactClass({ }, onJoinButtonClicked: function(ev) { - const cli = MatrixClientPeg.get(); - // If the user is a ROU, allow them to transition to a PWLU - if (cli && cli.isGuest()) { + if (this.context && this.context.isGuest()) { // Join this room once the user has registered and logged in // (If we failed to peek, we may not have a valid room object.) dis.dispatch({ @@ -1106,7 +1127,7 @@ export default createReactClass({ ev.stopPropagation(); ev.preventDefault(); ContentMessages.sharedInstance().sendContentListToRoom( - ev.dataTransfer.files, this.state.room.roomId, MatrixClientPeg.get(), + ev.dataTransfer.files, this.state.room.roomId, this.context, ); this.setState({ draggingFile: false }); dis.dispatch({action: 'focus_composer'}); @@ -1119,12 +1140,12 @@ export default createReactClass({ }, injectSticker: function(url, info, text) { - if (MatrixClientPeg.get().isGuest()) { + if (this.context.isGuest()) { dis.dispatch({action: 'require_registration'}); return; } - ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, MatrixClientPeg.get()) + ContentMessages.sharedInstance().sendStickerContentToRoom(url, this.state.room.roomId, info, text, this.context) .then(undefined, (error) => { if (error.name === "UnknownDeviceError") { // Let the staus bar handle this @@ -1215,12 +1236,9 @@ export default createReactClass({ }, getSearchResultTiles: function() { - const EventTile = sdk.getComponent('rooms.EventTile'); const SearchResultTile = sdk.getComponent('rooms.SearchResultTile'); const Spinner = sdk.getComponent("elements.Spinner"); - const cli = MatrixClientPeg.get(); - // XXX: todo: merge overlapping results somehow? // XXX: why doesn't searching on name work? @@ -1228,21 +1246,21 @@ export default createReactClass({ if (this.state.searchInProgress) { ret.push(
  • - -
  • ); + + ); } if (!this.state.searchResults.next_batch) { if (this.state.searchResults.results.length == 0) { ret.push(
  • -

    { _t("No results") }

    -
  • , - ); +

    { _t("No results") }

    + , + ); } else { ret.push(
  • -

    { _t("No more results") }

    -
  • , - ); +

    { _t("No more results") }

    + , + ); } } @@ -1262,7 +1280,7 @@ export default createReactClass({ const mxEv = result.context.getEvent(); const roomId = mxEv.getRoomId(); - const room = cli.getRoom(roomId); + const room = this.context.getRoom(roomId); if (!haveTileForEvent(mxEv)) { // XXX: can this ever happen? It will make the result count @@ -1329,7 +1347,7 @@ export default createReactClass({ }, onForgetClick: function() { - MatrixClientPeg.get().forget(this.state.room.roomId).then(function() { + this.context.forget(this.state.room.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); }, function(err) { const errCode = err.errcode || _t("unknown error code"); @@ -1346,7 +1364,7 @@ export default createReactClass({ this.setState({ rejecting: true, }); - MatrixClientPeg.get().leave(this.state.roomId).then(function() { + this.context.leave(this.state.roomId).then(function() { dis.dispatch({ action: 'view_next_room' }); self.setState({ rejecting: false, @@ -1373,15 +1391,14 @@ export default createReactClass({ rejecting: true, }); - const cli = MatrixClientPeg.get(); try { - const myMember = this.state.room.getMember(cli.getUserId()); + const myMember = this.state.room.getMember(this.context.getUserId()); const inviteEvent = myMember.events.member; - const ignoredUsers = MatrixClientPeg.get().getIgnoredUsers(); + const ignoredUsers = this.context.getIgnoredUsers(); ignoredUsers.push(inviteEvent.getSender()); // de-duped internally in the js-sdk - await cli.setIgnoredUsers(ignoredUsers); + await this.context.setIgnoredUsers(ignoredUsers); - await cli.leave(this.state.roomId); + await this.context.leave(this.state.roomId); dis.dispatch({ action: 'view_next_room' }); this.setState({ rejecting: false, @@ -1600,7 +1617,7 @@ export default createReactClass({ const createEvent = this.state.room.currentState.getStateEvents("m.room.create", ""); if (!createEvent || !createEvent.getContent()['predecessor']) return null; - return MatrixClientPeg.get().getRoom(createEvent.getContent()['predecessor']['room_id']); + return this.context.getRoom(createEvent.getContent()['predecessor']['room_id']); }, _getHiddenHighlightCount: function() { @@ -1694,7 +1711,7 @@ export default createReactClass({ ); } else { - const myUserId = MatrixClientPeg.get().credentials.userId; + const myUserId = this.context.credentials.userId; const myMember = this.state.room.getMember(myUserId); const inviteEvent = myMember.events.member; var inviterName = inviteEvent.sender ? inviteEvent.sender.name : inviteEvent.getSender(); @@ -1761,13 +1778,13 @@ export default createReactClass({ const showRoomUpgradeBar = ( roomVersionRecommendation && roomVersionRecommendation.needsUpgrade && - this.state.room.userMayUpgradeRoom(MatrixClientPeg.get().credentials.userId) + this.state.room.userMayUpgradeRoom(this.context.credentials.userId) ); const showRoomRecoveryReminder = ( SettingsStore.getValue("showRoomRecoveryReminder") && - MatrixClientPeg.get().isRoomEncrypted(this.state.room.roomId) && - !MatrixClientPeg.get().getKeyBackupEnabled() + this.context.isRoomEncrypted(this.state.room.roomId) && + !this.context.getKeyBackupEnabled() ); const hiddenHighlightCount = this._getHiddenHighlightCount(); @@ -1838,7 +1855,7 @@ export default createReactClass({ const auxPanel = (