Merge remote-tracking branch 'upstream/develop' into feature_confetti#14676
|
@ -1,51 +1,31 @@
|
|||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/structures/RoomStatusBar.js
|
||||
src/components/structures/RoomView.js
|
||||
src/components/structures/ScrollPanel.js
|
||||
src/components/structures/SearchBox.js
|
||||
src/components/structures/UploadBar.js
|
||||
src/components/views/avatars/MemberAvatar.js
|
||||
src/components/views/create_room/RoomAlias.js
|
||||
src/components/views/dialogs/SetPasswordDialog.js
|
||||
src/components/views/elements/AddressSelector.js
|
||||
src/components/views/elements/DirectorySearchBox.js
|
||||
src/components/views/elements/MemberEventListSummary.js
|
||||
src/components/views/elements/UserSelector.js
|
||||
src/components/views/globals/NewVersionBar.js
|
||||
src/components/views/messages/MFileBody.js
|
||||
src/components/views/messages/TextualBody.js
|
||||
src/components/views/room_settings/ColorSettings.js
|
||||
src/components/views/rooms/Autocomplete.js
|
||||
src/components/views/rooms/AuxPanel.js
|
||||
src/components/views/rooms/LinkPreviewWidget.js
|
||||
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/SearchResultTile.js
|
||||
src/components/views/settings/ChangeAvatar.js
|
||||
src/components/views/settings/ChangePassword.js
|
||||
src/components/views/settings/DevicesPanel.js
|
||||
src/components/views/settings/Notifications.js
|
||||
src/HtmlUtils.js
|
||||
src/ImageUtils.js
|
||||
src/Markdown.js
|
||||
src/notifications/ContentRules.js
|
||||
src/notifications/PushRuleVectorState.js
|
||||
src/PlatformPeg.js
|
||||
src/rageshake/rageshake.js
|
||||
src/ratelimitedfunc.js
|
||||
src/Rooms.js
|
||||
src/Unread.js
|
||||
src/Velociraptor.js
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/structures/ScrollPanel.js
|
||||
src/components/structures/UploadBar.js
|
||||
src/components/views/elements/AddressSelector.js
|
||||
src/components/views/elements/DirectorySearchBox.js
|
||||
src/components/views/messages/MFileBody.js
|
||||
src/components/views/messages/TextualBody.js
|
||||
src/components/views/rooms/AuxPanel.js
|
||||
src/components/views/rooms/LinkPreviewWidget.js
|
||||
src/components/views/rooms/MemberList.js
|
||||
src/components/views/rooms/RoomPreviewBar.js
|
||||
src/components/views/settings/ChangeAvatar.js
|
||||
src/components/views/settings/DevicesPanel.js
|
||||
src/components/views/settings/Notifications.js
|
||||
src/rageshake/rageshake.js
|
||||
src/ratelimitedfunc.js
|
||||
src/utils/DMRoomMap.js
|
||||
src/utils/DecryptFile.js
|
||||
src/utils/DirectoryUtils.js
|
||||
src/utils/DMRoomMap.js
|
||||
src/utils/FormattingUtils.js
|
||||
src/utils/MultiInviter.js
|
||||
src/utils/Receipt.js
|
||||
src/Velociraptor.js
|
||||
test/components/structures/MessagePanel-test.js
|
||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||
test/mock-clock.js
|
||||
|
|
378
CHANGELOG.md
|
@ -1,3 +1,381 @@
|
|||
Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0)
|
||||
|
||||
* Upgrade JS SDK to 8.5.0
|
||||
* [Release] Fix templating for v1 jitsi widgets
|
||||
[\#5306](https://github.com/matrix-org/matrix-react-sdk/pull/5306)
|
||||
* [Release] Use new preparing event for widget communications
|
||||
[\#5304](https://github.com/matrix-org/matrix-react-sdk/pull/5304)
|
||||
|
||||
Changes in [3.6.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0-rc.1) (2020-10-07)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0...v3.6.0-rc.1)
|
||||
|
||||
* Upgrade JS SDK to 8.5.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#5297](https://github.com/matrix-org/matrix-react-sdk/pull/5297)
|
||||
* Fix edited replies being wrongly treated as big emoji
|
||||
[\#5295](https://github.com/matrix-org/matrix-react-sdk/pull/5295)
|
||||
* Fix StopGapWidget infinitely recursing
|
||||
[\#5294](https://github.com/matrix-org/matrix-react-sdk/pull/5294)
|
||||
* Fix editing and redactions not updating the Reply Thread
|
||||
[\#5281](https://github.com/matrix-org/matrix-react-sdk/pull/5281)
|
||||
* Hide Jump to Read Receipt button for users who have not yet sent an RR
|
||||
[\#5282](https://github.com/matrix-org/matrix-react-sdk/pull/5282)
|
||||
* fix img tags not always being rendered correctly
|
||||
[\#5279](https://github.com/matrix-org/matrix-react-sdk/pull/5279)
|
||||
* Hopefully fix righhtpanel crash
|
||||
[\#5293](https://github.com/matrix-org/matrix-react-sdk/pull/5293)
|
||||
* Fix naive pinning limit and app tile widgetMessaging NPE
|
||||
[\#5283](https://github.com/matrix-org/matrix-react-sdk/pull/5283)
|
||||
* Show server errors from saving profile settings
|
||||
[\#5272](https://github.com/matrix-org/matrix-react-sdk/pull/5272)
|
||||
* Update copy for `redact` permission
|
||||
[\#5273](https://github.com/matrix-org/matrix-react-sdk/pull/5273)
|
||||
* Remove width limit on widgets
|
||||
[\#5265](https://github.com/matrix-org/matrix-react-sdk/pull/5265)
|
||||
* Fix call container avatar initial centering
|
||||
[\#5280](https://github.com/matrix-org/matrix-react-sdk/pull/5280)
|
||||
* Fix right panel for peeking rooms
|
||||
[\#5268](https://github.com/matrix-org/matrix-react-sdk/pull/5268)
|
||||
* Add support for dehydrated devices
|
||||
[\#5239](https://github.com/matrix-org/matrix-react-sdk/pull/5239)
|
||||
* Use Own Profile Store for the Profile Settings
|
||||
[\#5277](https://github.com/matrix-org/matrix-react-sdk/pull/5277)
|
||||
* null-guard defaultAvatarUrlForString
|
||||
[\#5270](https://github.com/matrix-org/matrix-react-sdk/pull/5270)
|
||||
* Choose first result on enter in the emoji picker
|
||||
[\#5257](https://github.com/matrix-org/matrix-react-sdk/pull/5257)
|
||||
* Fix room directory clipping links in the room's topic
|
||||
[\#5276](https://github.com/matrix-org/matrix-react-sdk/pull/5276)
|
||||
* Decorate failed e2ee downgrade attempts better
|
||||
[\#5278](https://github.com/matrix-org/matrix-react-sdk/pull/5278)
|
||||
* MELS use latest avatar rather than the first avatar
|
||||
[\#5262](https://github.com/matrix-org/matrix-react-sdk/pull/5262)
|
||||
* Fix Encryption Panel close button clashing with Base Card
|
||||
[\#5261](https://github.com/matrix-org/matrix-react-sdk/pull/5261)
|
||||
* Wrap canEncryptToAllUsers in a try/catch to handle server errors
|
||||
[\#5275](https://github.com/matrix-org/matrix-react-sdk/pull/5275)
|
||||
* Fix conditional on communities prototype room creation dialog
|
||||
[\#5274](https://github.com/matrix-org/matrix-react-sdk/pull/5274)
|
||||
* Fix ensureDmExists for encryption detection
|
||||
[\#5271](https://github.com/matrix-org/matrix-react-sdk/pull/5271)
|
||||
* Switch to using the Widget API SDK for widget messaging
|
||||
[\#5171](https://github.com/matrix-org/matrix-react-sdk/pull/5171)
|
||||
* Ensure package links exist when releasing
|
||||
[\#5269](https://github.com/matrix-org/matrix-react-sdk/pull/5269)
|
||||
* Fix the call preview when not in same room as the call
|
||||
[\#5267](https://github.com/matrix-org/matrix-react-sdk/pull/5267)
|
||||
* Make the hangup button do things for conference calls
|
||||
[\#5223](https://github.com/matrix-org/matrix-react-sdk/pull/5223)
|
||||
* Render Jitsi widget state events in a more obvious way
|
||||
[\#5222](https://github.com/matrix-org/matrix-react-sdk/pull/5222)
|
||||
* Make the PIP Jitsi look and feel like the 1:1 PIP
|
||||
[\#5226](https://github.com/matrix-org/matrix-react-sdk/pull/5226)
|
||||
* Trim range when formatting so that it excludes leading/trailing spaces
|
||||
[\#5263](https://github.com/matrix-org/matrix-react-sdk/pull/5263)
|
||||
* Fix button label on the Set Password Dialog
|
||||
[\#5264](https://github.com/matrix-org/matrix-react-sdk/pull/5264)
|
||||
* fix link to classic yarn's `yarn link`
|
||||
[\#5259](https://github.com/matrix-org/matrix-react-sdk/pull/5259)
|
||||
* Fix index mismatch between username colors styles and custom theming
|
||||
[\#5256](https://github.com/matrix-org/matrix-react-sdk/pull/5256)
|
||||
* Disable autocompletion on security key input during login
|
||||
[\#5258](https://github.com/matrix-org/matrix-react-sdk/pull/5258)
|
||||
* fix uninitialised state and eventlistener leak in RoomUpgradeWarningBar
|
||||
[\#5255](https://github.com/matrix-org/matrix-react-sdk/pull/5255)
|
||||
* Only set title when it changes
|
||||
[\#5254](https://github.com/matrix-org/matrix-react-sdk/pull/5254)
|
||||
* Convert CallHandler to typescript
|
||||
[\#5248](https://github.com/matrix-org/matrix-react-sdk/pull/5248)
|
||||
* Retry loading i18n language if it fails
|
||||
[\#5209](https://github.com/matrix-org/matrix-react-sdk/pull/5209)
|
||||
* Rework profile area for user and room settings to be more clear
|
||||
[\#5243](https://github.com/matrix-org/matrix-react-sdk/pull/5243)
|
||||
* Validation improve pattern for derived data
|
||||
[\#5241](https://github.com/matrix-org/matrix-react-sdk/pull/5241)
|
||||
|
||||
Changes in [3.5.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0) (2020-09-28)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.5.0-rc.1...v3.5.0)
|
||||
|
||||
* Upgrade JS SDK to 8.4.1
|
||||
|
||||
Changes in [3.5.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.5.0-rc.1) (2020-09-23)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.1...v3.5.0-rc.1)
|
||||
|
||||
* Upgrade JS SDK to 8.4.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#5246](https://github.com/matrix-org/matrix-react-sdk/pull/5246)
|
||||
* Upgrade sanitize-html, set nesting limit
|
||||
[\#5245](https://github.com/matrix-org/matrix-react-sdk/pull/5245)
|
||||
* Add a note to use the desktop builds when seshat isn't available
|
||||
[\#5225](https://github.com/matrix-org/matrix-react-sdk/pull/5225)
|
||||
* Add some permission checks to the communities v2 prototype
|
||||
[\#5240](https://github.com/matrix-org/matrix-react-sdk/pull/5240)
|
||||
* Support HS-preferred Secure Backup setup methods
|
||||
[\#5242](https://github.com/matrix-org/matrix-react-sdk/pull/5242)
|
||||
* Only show User Info verify button if the other user has e2ee devices
|
||||
[\#5234](https://github.com/matrix-org/matrix-react-sdk/pull/5234)
|
||||
* Fix New Room List arrow key management
|
||||
[\#5237](https://github.com/matrix-org/matrix-react-sdk/pull/5237)
|
||||
* Fix Room Directory View & Preview actions for federated joins
|
||||
[\#5235](https://github.com/matrix-org/matrix-react-sdk/pull/5235)
|
||||
* Add a UI feature to disable advanced encryption options
|
||||
[\#5238](https://github.com/matrix-org/matrix-react-sdk/pull/5238)
|
||||
* UI Feature Flag: Communities
|
||||
[\#5216](https://github.com/matrix-org/matrix-react-sdk/pull/5216)
|
||||
* Rename apps back to widgets
|
||||
[\#5236](https://github.com/matrix-org/matrix-react-sdk/pull/5236)
|
||||
* Adjust layout and formatting of notifications / files cards
|
||||
[\#5229](https://github.com/matrix-org/matrix-react-sdk/pull/5229)
|
||||
* Fix Search Results Tile undefined variable access regression
|
||||
[\#5232](https://github.com/matrix-org/matrix-react-sdk/pull/5232)
|
||||
* Fix Cmd/Ctrl+Shift+U for File Upload
|
||||
[\#5233](https://github.com/matrix-org/matrix-react-sdk/pull/5233)
|
||||
* Disable the e2ee toggle when creating a room on a server with forced e2e
|
||||
[\#5231](https://github.com/matrix-org/matrix-react-sdk/pull/5231)
|
||||
* UI Feature Flag: Disable advanced options and tidy up some copy
|
||||
[\#5215](https://github.com/matrix-org/matrix-react-sdk/pull/5215)
|
||||
* UI Feature Flag: 3PIDs
|
||||
[\#5228](https://github.com/matrix-org/matrix-react-sdk/pull/5228)
|
||||
* Defer encryption setup until first E2EE room
|
||||
[\#5219](https://github.com/matrix-org/matrix-react-sdk/pull/5219)
|
||||
* Tidy devDeps, all the webpack stuff lives in the layer above
|
||||
[\#5179](https://github.com/matrix-org/matrix-react-sdk/pull/5179)
|
||||
* UI Feature Flag: Hide flair
|
||||
[\#5214](https://github.com/matrix-org/matrix-react-sdk/pull/5214)
|
||||
* UI Feature Flag: Identity server
|
||||
[\#5218](https://github.com/matrix-org/matrix-react-sdk/pull/5218)
|
||||
* UI Feature Flag: Share dialog QR code and social icons
|
||||
[\#5221](https://github.com/matrix-org/matrix-react-sdk/pull/5221)
|
||||
* UI Feature Flag: Registration, Password Reset, Deactivate
|
||||
[\#5227](https://github.com/matrix-org/matrix-react-sdk/pull/5227)
|
||||
* Retry joinRoom up to 5 times in the case of a 504 GATEWAY TIMEOUT
|
||||
[\#5204](https://github.com/matrix-org/matrix-react-sdk/pull/5204)
|
||||
* UI Feature Flag: Disable VoIP
|
||||
[\#5217](https://github.com/matrix-org/matrix-react-sdk/pull/5217)
|
||||
* Fix setState() usage in the constructor of RoomDirectory
|
||||
[\#5224](https://github.com/matrix-org/matrix-react-sdk/pull/5224)
|
||||
* Hide Analytics sections if piwik config is not provided
|
||||
[\#5211](https://github.com/matrix-org/matrix-react-sdk/pull/5211)
|
||||
* UI Feature Flag: Disable feedback button
|
||||
[\#5213](https://github.com/matrix-org/matrix-react-sdk/pull/5213)
|
||||
* Clean up UserInfo to not show a blank Power Selector for users not in room
|
||||
[\#5220](https://github.com/matrix-org/matrix-react-sdk/pull/5220)
|
||||
* Also hide bug reporting prompts from the Error Boundaries
|
||||
[\#5212](https://github.com/matrix-org/matrix-react-sdk/pull/5212)
|
||||
* Tactical improvements to 3PID invites
|
||||
[\#5201](https://github.com/matrix-org/matrix-react-sdk/pull/5201)
|
||||
* If no bug_report_endpoint_url, hide rageshaking from the App
|
||||
[\#5210](https://github.com/matrix-org/matrix-react-sdk/pull/5210)
|
||||
* Introduce a concept of UI features, using it for URL previews at first
|
||||
[\#5208](https://github.com/matrix-org/matrix-react-sdk/pull/5208)
|
||||
* Remove defunct "always show encryption icons" setting
|
||||
[\#5207](https://github.com/matrix-org/matrix-react-sdk/pull/5207)
|
||||
* Don't show Notifications Prompt Toast if user has master rule enabled
|
||||
[\#5203](https://github.com/matrix-org/matrix-react-sdk/pull/5203)
|
||||
* Fix Bridges tab crashing when the room does not have bridges
|
||||
[\#5206](https://github.com/matrix-org/matrix-react-sdk/pull/5206)
|
||||
* Don't count widgets which no longer exist towards pinned count
|
||||
[\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202)
|
||||
* Fix crashes with cannot read isResizing of undefined
|
||||
[\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205)
|
||||
* Prompt to remove the jitsi widget when pressing the call button
|
||||
[\#5193](https://github.com/matrix-org/matrix-react-sdk/pull/5193)
|
||||
* Show verification status in the room summary card
|
||||
[\#5195](https://github.com/matrix-org/matrix-react-sdk/pull/5195)
|
||||
* Fix user info scrolling in new card view
|
||||
[\#5198](https://github.com/matrix-org/matrix-react-sdk/pull/5198)
|
||||
* Fix sticker picker height
|
||||
[\#5197](https://github.com/matrix-org/matrix-react-sdk/pull/5197)
|
||||
* Call jitsi widgets 'group calls'
|
||||
[\#5191](https://github.com/matrix-org/matrix-react-sdk/pull/5191)
|
||||
* Don't show 'unpin' for persistent widgets
|
||||
[\#5194](https://github.com/matrix-org/matrix-react-sdk/pull/5194)
|
||||
* Split up cross-signing and secure backup settings
|
||||
[\#5182](https://github.com/matrix-org/matrix-react-sdk/pull/5182)
|
||||
* Fix onNewScreen to use replace when going from roomId->roomAlias
|
||||
[\#5185](https://github.com/matrix-org/matrix-react-sdk/pull/5185)
|
||||
* bring back 1.2M style badge counts rather than 99+
|
||||
[\#5192](https://github.com/matrix-org/matrix-react-sdk/pull/5192)
|
||||
* Run the rageshake command through the bug report dialog
|
||||
[\#5189](https://github.com/matrix-org/matrix-react-sdk/pull/5189)
|
||||
* Account for via in pill matching regex
|
||||
[\#5188](https://github.com/matrix-org/matrix-react-sdk/pull/5188)
|
||||
* Remove now-unused create-react-class from lockfile
|
||||
[\#5187](https://github.com/matrix-org/matrix-react-sdk/pull/5187)
|
||||
* Fixed 1px jump upwards
|
||||
[\#5163](https://github.com/matrix-org/matrix-react-sdk/pull/5163)
|
||||
* Always allow widgets when using the local version
|
||||
[\#5184](https://github.com/matrix-org/matrix-react-sdk/pull/5184)
|
||||
* Migrate RoomView and RoomContext to Typescript
|
||||
[\#5175](https://github.com/matrix-org/matrix-react-sdk/pull/5175)
|
||||
|
||||
Changes in [3.4.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.1) (2020-09-14)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0...v3.4.1)
|
||||
|
||||
* Don't count widgets which no longer exist towards pinned count
|
||||
[\#5202](https://github.com/matrix-org/matrix-react-sdk/pull/5202)
|
||||
* Fix crashes with cannot read isResizing of undefined
|
||||
[\#5205](https://github.com/matrix-org/matrix-react-sdk/pull/5205)
|
||||
|
||||
Changes in [3.4.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.0) (2020-09-14)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.4.0-rc.1...v3.4.0)
|
||||
|
||||
* Upgrade to JS SDK 8.3.0
|
||||
* [Release] Show verification status in the room summary card
|
||||
[\#5196](https://github.com/matrix-org/matrix-react-sdk/pull/5196)
|
||||
* Fix user info scrolling in new card view
|
||||
[\#5200](https://github.com/matrix-org/matrix-react-sdk/pull/5200)
|
||||
* Fix sticker picker height
|
||||
[\#5199](https://github.com/matrix-org/matrix-react-sdk/pull/5199)
|
||||
* [Release] Account for via in pill matching regex
|
||||
[\#5190](https://github.com/matrix-org/matrix-react-sdk/pull/5190)
|
||||
|
||||
Changes in [3.4.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.4.0-rc.1) (2020-09-09)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.3.0...v3.4.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 8.3.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#5183](https://github.com/matrix-org/matrix-react-sdk/pull/5183)
|
||||
* Right Panel Room Summary and Widgets
|
||||
[\#5167](https://github.com/matrix-org/matrix-react-sdk/pull/5167)
|
||||
* null-guard roomId in RightPanel and pass Room to UserView
|
||||
[\#5180](https://github.com/matrix-org/matrix-react-sdk/pull/5180)
|
||||
* Fix create-react-class regression.
|
||||
[\#5178](https://github.com/matrix-org/matrix-react-sdk/pull/5178)
|
||||
* Fix WatchManager for global room watchers and tidy widget code a little
|
||||
[\#5176](https://github.com/matrix-org/matrix-react-sdk/pull/5176)
|
||||
* Fix permalink local linkification to not strip via servers
|
||||
[\#5174](https://github.com/matrix-org/matrix-react-sdk/pull/5174)
|
||||
* Support creation of Jitsi widgets with "openidtoken-jwt" auth
|
||||
[\#5173](https://github.com/matrix-org/matrix-react-sdk/pull/5173)
|
||||
* Fix create-react-class regression.
|
||||
[\#5177](https://github.com/matrix-org/matrix-react-sdk/pull/5177)
|
||||
* Update openid_credentials Widget API action for MSC1960 updates
|
||||
[\#5172](https://github.com/matrix-org/matrix-react-sdk/pull/5172)
|
||||
* Allow persistent resizing of the widget app drawer
|
||||
[\#5138](https://github.com/matrix-org/matrix-react-sdk/pull/5138)
|
||||
* add lenny face command
|
||||
[\#5158](https://github.com/matrix-org/matrix-react-sdk/pull/5158)
|
||||
* Prep work for Settings changes with cross-signing deferral
|
||||
[\#5169](https://github.com/matrix-org/matrix-react-sdk/pull/5169)
|
||||
* Small code clean ups and tweaks
|
||||
[\#5168](https://github.com/matrix-org/matrix-react-sdk/pull/5168)
|
||||
* Fix soft crash from TruncatedList in the createReactClass conversion
|
||||
[\#5170](https://github.com/matrix-org/matrix-react-sdk/pull/5170)
|
||||
* Remove create-react-class
|
||||
[\#5157](https://github.com/matrix-org/matrix-react-sdk/pull/5157)
|
||||
* Consolidate Lodash files in bundle
|
||||
[\#5162](https://github.com/matrix-org/matrix-react-sdk/pull/5162)
|
||||
* Communities v2 prototype: "In community" view
|
||||
[\#5161](https://github.com/matrix-org/matrix-react-sdk/pull/5161)
|
||||
* Respect user preference for whether pills should have an avatar or not
|
||||
[\#5165](https://github.com/matrix-org/matrix-react-sdk/pull/5165)
|
||||
* Communities v2 prototype: DM copy updates
|
||||
[\#5153](https://github.com/matrix-org/matrix-react-sdk/pull/5153)
|
||||
* Only wait for public keys during verification
|
||||
[\#5164](https://github.com/matrix-org/matrix-react-sdk/pull/5164)
|
||||
* Fix eslint ts override tsx matching and delint
|
||||
[\#5155](https://github.com/matrix-org/matrix-react-sdk/pull/5155)
|
||||
* Fix react error about functional components can't take refs
|
||||
[\#5159](https://github.com/matrix-org/matrix-react-sdk/pull/5159)
|
||||
* Remove redundant components and devDependencies
|
||||
[\#5156](https://github.com/matrix-org/matrix-react-sdk/pull/5156)
|
||||
* Add display-capture to iframe allow for widgets
|
||||
[\#5154](https://github.com/matrix-org/matrix-react-sdk/pull/5154)
|
||||
* Update create room dialog copy & community prototype home icon
|
||||
[\#5151](https://github.com/matrix-org/matrix-react-sdk/pull/5151)
|
||||
* Migrate to new, separate APIs for cross-signing and secret storage
|
||||
[\#5149](https://github.com/matrix-org/matrix-react-sdk/pull/5149)
|
||||
* Fix clicking the background of the tag panel not clearing the filter
|
||||
[\#5152](https://github.com/matrix-org/matrix-react-sdk/pull/5152)
|
||||
* Communities v2 prototype: Associate created rooms with the selected
|
||||
community
|
||||
[\#5147](https://github.com/matrix-org/matrix-react-sdk/pull/5147)
|
||||
* Communities v2 prototype: Tag panel selection changes
|
||||
[\#5145](https://github.com/matrix-org/matrix-react-sdk/pull/5145)
|
||||
* Communities v2 prototype: Create community flow
|
||||
[\#5144](https://github.com/matrix-org/matrix-react-sdk/pull/5144)
|
||||
* Communities v2 prototype: Override invite aesthetics for community-as-room
|
||||
invites
|
||||
[\#5143](https://github.com/matrix-org/matrix-react-sdk/pull/5143)
|
||||
|
||||
Changes in [3.3.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.3.0) (2020-09-01)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.3.0-rc.1...v3.3.0)
|
||||
|
||||
* Upgrade to JS SDK 8.2.0
|
||||
|
||||
Changes in [3.3.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.3.0-rc.1) (2020-08-26)
|
||||
=============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.2.0...v3.3.0-rc.1)
|
||||
|
||||
* Upgrade to JS SDK 8.2.0-rc.1
|
||||
* Update from Weblate
|
||||
[\#5146](https://github.com/matrix-org/matrix-react-sdk/pull/5146)
|
||||
* BaseAvatar avoid initial render with default avatar
|
||||
[\#5142](https://github.com/matrix-org/matrix-react-sdk/pull/5142)
|
||||
* Enforce Secure Backup completion when requested by HS
|
||||
[\#5130](https://github.com/matrix-org/matrix-react-sdk/pull/5130)
|
||||
* Communities v2 prototype: Explore rooms, global state, and default room
|
||||
[\#5139](https://github.com/matrix-org/matrix-react-sdk/pull/5139)
|
||||
* Add communities v2 prototyping feature flag + initial tag panel prototypes
|
||||
[\#5133](https://github.com/matrix-org/matrix-react-sdk/pull/5133)
|
||||
* Remove some unused components
|
||||
[\#5134](https://github.com/matrix-org/matrix-react-sdk/pull/5134)
|
||||
* Allow avatar image view for 1:1 rooms
|
||||
[\#5137](https://github.com/matrix-org/matrix-react-sdk/pull/5137)
|
||||
* Send mx_local_settings in rageshake
|
||||
[\#5136](https://github.com/matrix-org/matrix-react-sdk/pull/5136)
|
||||
* Run all room leaving behaviour through a single function
|
||||
[\#5132](https://github.com/matrix-org/matrix-react-sdk/pull/5132)
|
||||
* Add clarifying comment in media device selection
|
||||
[\#5131](https://github.com/matrix-org/matrix-react-sdk/pull/5131)
|
||||
* Settings v3: Feature flag changes
|
||||
[\#5124](https://github.com/matrix-org/matrix-react-sdk/pull/5124)
|
||||
* Clear url previews if they all get edited out of the event
|
||||
[\#5129](https://github.com/matrix-org/matrix-react-sdk/pull/5129)
|
||||
* Consider tab completions as modifications for editing purposes to unlock
|
||||
sending
|
||||
[\#5128](https://github.com/matrix-org/matrix-react-sdk/pull/5128)
|
||||
* Use matrix-doc for SAS emoji translations
|
||||
[\#5125](https://github.com/matrix-org/matrix-react-sdk/pull/5125)
|
||||
* Add a rageshake function to download the logs locally
|
||||
[\#3849](https://github.com/matrix-org/matrix-react-sdk/pull/3849)
|
||||
* Room List filtering visual tweaks
|
||||
[\#5123](https://github.com/matrix-org/matrix-react-sdk/pull/5123)
|
||||
* Make reply preview not an overlay so you can see new messages
|
||||
[\#5072](https://github.com/matrix-org/matrix-react-sdk/pull/5072)
|
||||
* Allow room tile context menu when minimized using right click
|
||||
[\#5113](https://github.com/matrix-org/matrix-react-sdk/pull/5113)
|
||||
* Add null guard to group inviter for corrupted groups
|
||||
[\#5121](https://github.com/matrix-org/matrix-react-sdk/pull/5121)
|
||||
* Room List styling tweaks
|
||||
[\#5118](https://github.com/matrix-org/matrix-react-sdk/pull/5118)
|
||||
* Fix corner rounding on images not always affecting right side
|
||||
[\#5120](https://github.com/matrix-org/matrix-react-sdk/pull/5120)
|
||||
* Change add room action for rooms to context menu
|
||||
[\#5108](https://github.com/matrix-org/matrix-react-sdk/pull/5108)
|
||||
* Switch out the globe icon and colour it depending on theme
|
||||
[\#5106](https://github.com/matrix-org/matrix-react-sdk/pull/5106)
|
||||
* Message Action Bar watch for event send changes
|
||||
[\#5115](https://github.com/matrix-org/matrix-react-sdk/pull/5115)
|
||||
* Put message previews for Emoji behind Labs
|
||||
[\#5110](https://github.com/matrix-org/matrix-react-sdk/pull/5110)
|
||||
* Fix styling for selected community marker
|
||||
[\#5107](https://github.com/matrix-org/matrix-react-sdk/pull/5107)
|
||||
* Fix action bar safe area regression
|
||||
[\#5111](https://github.com/matrix-org/matrix-react-sdk/pull/5111)
|
||||
* Fix /op slash command
|
||||
[\#5109](https://github.com/matrix-org/matrix-react-sdk/pull/5109)
|
||||
|
||||
Changes in [3.2.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.2.0) (2020-08-17)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.2.0-rc.1...v3.2.0)
|
||||
|
|
|
@ -160,8 +160,8 @@ yarn link matrix-js-sdk
|
|||
yarn install
|
||||
```
|
||||
|
||||
See the [help for `yarn link`](https://yarnpkg.com/docs/cli/link) for more
|
||||
details about this.
|
||||
See the [help for `yarn link`](https://classic.yarnpkg.com/docs/cli/link) for
|
||||
more details about this.
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
const en = require("../src/i18n/strings/en_EN");
|
||||
const de = require("../src/i18n/strings/de_DE");
|
||||
|
||||
// Mock the browser-request for the languageHandler tests to return
|
||||
// Fake languages.json containing references to en_EN and de_DE
|
||||
// en_EN.json
|
||||
// de_DE.json
|
||||
module.exports = jest.fn((opts, cb) => {
|
||||
const url = opts.url || opts.uri;
|
||||
if (url && url.endsWith("languages.json")) {
|
||||
|
@ -8,9 +13,15 @@ module.exports = jest.fn((opts, cb) => {
|
|||
"fileName": "en_EN.json",
|
||||
"label": "English",
|
||||
},
|
||||
"de": {
|
||||
"fileName": "de_DE.json",
|
||||
"label": "German",
|
||||
},
|
||||
}));
|
||||
} else if (url && url.endsWith("en_EN.json")) {
|
||||
cb(undefined, {status: 200}, JSON.stringify(en));
|
||||
} else if (url && url.endsWith("de_DE.json")) {
|
||||
cb(undefined, {status: 200}, JSON.stringify(de));
|
||||
} else {
|
||||
cb(true, {status: 404}, "");
|
||||
}
|
||||
|
|
17
__test-utils__/environment.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
const BaseEnvironment = require("jest-environment-jsdom-sixteen");
|
||||
|
||||
class Environment extends BaseEnvironment {
|
||||
constructor(config, options) {
|
||||
super(Object.assign({}, config, {
|
||||
globals: Object.assign({}, config.globals, {
|
||||
// Explicitly specify the correct globals to workaround Jest bug
|
||||
// https://github.com/facebook/jest/issues/7780
|
||||
Uint32Array: Uint32Array,
|
||||
Uint8Array: Uint8Array,
|
||||
ArrayBuffer: ArrayBuffer,
|
||||
}),
|
||||
}), options);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Environment;
|
|
@ -120,6 +120,18 @@ Call `SettingsStore.getValue()` as you would for any other setting.
|
|||
|
||||
Call `SettingsStore.setValue("feature_name", null, SettingLevel.DEVICE, true)`.
|
||||
|
||||
### A note on UI features
|
||||
|
||||
UI features are a different concept to plain features. Instead of being representative of unstable or
|
||||
unpredicatable behaviour, they are logical chunks of UI which can be disabled by deployments for ease
|
||||
of understanding with users. They are simply represented as boring settings with a convention of being
|
||||
named as `UIFeature.$location` where `$location` is a rough descriptor of what is toggled, such as
|
||||
`URLPreviews` or `Communities`.
|
||||
|
||||
UI features also tend to have their own setting controller (see below) to manipulate settings which might
|
||||
be affected by the UI feature being disabled. For example, if URL previews are disabled as a UI feature
|
||||
then the URL preview options will use the `UIFeatureController` to ensure they remain disabled while the
|
||||
UI feature is disabled.
|
||||
|
||||
## Setting controllers
|
||||
|
||||
|
@ -226,4 +238,3 @@ In practice, handlers which rely on remote changes (account data, room events, e
|
|||
generalized `WatchManager` - a class specifically designed to deduplicate the logic of managing watchers. The handlers
|
||||
which are localized to the local client (device) generally just trigger the `WatchManager` when they manipulate the
|
||||
setting themselves as there's nothing to really 'watch'.
|
||||
|
||||
|
|
21
package.json
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.2.0",
|
||||
"version": "3.6.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -61,7 +61,6 @@
|
|||
"classnames": "^2.2.6",
|
||||
"commonmark": "^0.29.1",
|
||||
"counterpart": "^0.18.6",
|
||||
"create-react-class": "^15.6.3",
|
||||
"diff-dom": "^4.1.6",
|
||||
"diff-match-patch": "^1.0.5",
|
||||
"emojibase-data": "^5.0.1",
|
||||
|
@ -80,6 +79,7 @@
|
|||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.19",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^0.1.0-beta.3",
|
||||
"minimist": "^1.2.5",
|
||||
"pako": "^1.0.11",
|
||||
"parse5": "^5.1.1",
|
||||
|
@ -95,7 +95,8 @@
|
|||
"react-focus-lock": "^2.4.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sanitize-html": "^1.27.1",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-html": "github:apostrophecms/sanitize-html#3c7f93f2058f696f5359e3e58d464161647226db",
|
||||
"tar-js": "^0.3.0",
|
||||
"text-encoding-utf-8": "^1.0.2",
|
||||
"url": "^0.11.0",
|
||||
|
@ -120,7 +121,7 @@
|
|||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@babel/register": "^7.10.5",
|
||||
"@babel/traverse": "^7.11.0",
|
||||
"@peculiar/webcrypto": "^1.1.2",
|
||||
"@peculiar/webcrypto": "^1.1.3",
|
||||
"@types/classnames": "^2.2.10",
|
||||
"@types/counterpart": "^0.18.1",
|
||||
"@types/flux": "^3.1.9",
|
||||
|
@ -149,25 +150,23 @@
|
|||
"eslint-plugin-flowtype": "^2.50.3",
|
||||
"eslint-plugin-react": "^7.20.3",
|
||||
"eslint-plugin-react-hooks": "^2.5.1",
|
||||
"file-loader": "^3.0.1",
|
||||
"glob": "^5.0.15",
|
||||
"jest": "^24.9.0",
|
||||
"jest-canvas-mock": "^2.2.0",
|
||||
"jest": "^26.5.2",
|
||||
"jest-canvas-mock": "^2.3.0",
|
||||
"jest-environment-jsdom-sixteen": "^1.0.3",
|
||||
"lolex": "^5.1.2",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"matrix-react-test-utils": "^0.2.2",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"rimraf": "^2.7.1",
|
||||
"source-map-loader": "^0.2.4",
|
||||
"stylelint": "^9.10.1",
|
||||
"stylelint-config-standard": "^18.3.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"typescript": "^3.9.7",
|
||||
"walk": "^2.3.14",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"walk": "^2.3.14"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "./__test-utils__/environment.js",
|
||||
"testMatch": [
|
||||
"<rootDir>/test/**/*-test.js"
|
||||
],
|
||||
|
|
|
@ -9,6 +9,9 @@ set -e
|
|||
|
||||
cd `dirname $0`
|
||||
|
||||
# This link seems to get eaten by the release process, so ensure it exists.
|
||||
yarn link matrix-js-sdk
|
||||
|
||||
for i in matrix-js-sdk
|
||||
do
|
||||
echo "Checking version of $i..."
|
||||
|
|
|
@ -18,6 +18,8 @@ limitations under the License.
|
|||
|
||||
@import "./_font-sizes.scss";
|
||||
|
||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
||||
|
||||
:root {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
@ -206,12 +208,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
border: 0;
|
||||
}
|
||||
|
||||
/* applied to side-panels and messagepanel when in RoomSettings */
|
||||
.mx_fadable {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
@ -260,7 +256,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
font-weight: 300;
|
||||
font-size: $font-15px;
|
||||
position: relative;
|
||||
padding: 25px 30px 30px 30px;
|
||||
padding: 24px;
|
||||
max-height: 80%;
|
||||
box-shadow: 2px 15px 30px 0 $dialog-shadow-color;
|
||||
border-radius: 8px;
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
@import "./structures/_ScrollPanel.scss";
|
||||
@import "./structures/_SearchBox.scss";
|
||||
@import "./structures/_TabbedView.scss";
|
||||
@import "./structures/_TagPanel.scss";
|
||||
@import "./structures/_GroupFilterPanel.scss";
|
||||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
@import "./structures/_UserMenu.scss";
|
||||
|
@ -61,11 +61,14 @@
|
|||
@import "./views/dialogs/_BugReportDialog.scss";
|
||||
@import "./views/dialogs/_ChangelogDialog.scss";
|
||||
@import "./views/dialogs/_ChatCreateOrReuseChatDialog.scss";
|
||||
@import "./views/dialogs/_CommunityPrototypeInviteDialog.scss";
|
||||
@import "./views/dialogs/_ConfirmUserActionDialog.scss";
|
||||
@import "./views/dialogs/_CreateCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_CreateGroupDialog.scss";
|
||||
@import "./views/dialogs/_CreateRoomDialog.scss";
|
||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
|
@ -78,8 +81,6 @@
|
|||
@import "./views/dialogs/_RoomUpgradeWarningDialog.scss";
|
||||
@import "./views/dialogs/_ServerOfflineDialog.scss";
|
||||
@import "./views/dialogs/_SetEmailDialog.scss";
|
||||
@import "./views/dialogs/_SetMxIdDialog.scss";
|
||||
@import "./views/dialogs/_SetPasswordDialog.scss";
|
||||
@import "./views/dialogs/_SettingsDialog.scss";
|
||||
@import "./views/dialogs/_ShareDialog.scss";
|
||||
@import "./views/dialogs/_SlashCommandHelpDialog.scss";
|
||||
|
@ -88,15 +89,17 @@
|
|||
@import "./views/dialogs/_UploadConfirmDialog.scss";
|
||||
@import "./views/dialogs/_UserSettingsDialog.scss";
|
||||
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
|
||||
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/secretstorage/_AccessSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/secretstorage/_CreateSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/security/_CreateCrossSigningDialog.scss";
|
||||
@import "./views/dialogs/security/_CreateKeyBackupDialog.scss";
|
||||
@import "./views/dialogs/security/_CreateSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/security/_KeyBackupFailedDialog.scss";
|
||||
@import "./views/dialogs/security/_RestoreKeyBackupDialog.scss";
|
||||
@import "./views/directory/_NetworkDropdown.scss";
|
||||
@import "./views/elements/_AccessibleButton.scss";
|
||||
@import "./views/elements/_AddressSelector.scss";
|
||||
@import "./views/elements/_AddressTile.scss";
|
||||
@import "./views/elements/_DesktopBuildsNotice.scss";
|
||||
@import "./views/elements/_DirectorySearchBox.scss";
|
||||
@import "./views/elements/_Dropdown.scss";
|
||||
@import "./views/elements/_EditableItemList.scss";
|
||||
|
@ -106,6 +109,7 @@
|
|||
@import "./views/elements/_FormButton.scss";
|
||||
@import "./views/elements/_IconButton.scss";
|
||||
@import "./views/elements/_ImageView.scss";
|
||||
@import "./views/elements/_InfoTooltip.scss";
|
||||
@import "./views/elements/_InlineSpinner.scss";
|
||||
@import "./views/elements/_ManageIntegsButton.scss";
|
||||
@import "./views/elements/_PowerSelector.scss";
|
||||
|
@ -135,6 +139,7 @@
|
|||
@import "./views/messages/_MEmoteBody.scss";
|
||||
@import "./views/messages/_MFileBody.scss";
|
||||
@import "./views/messages/_MImageBody.scss";
|
||||
@import "./views/messages/_MJitsiWidgetEvent.scss";
|
||||
@import "./views/messages/_MNoticeBody.scss";
|
||||
@import "./views/messages/_MStickerBody.scss";
|
||||
@import "./views/messages/_MTextBody.scss";
|
||||
|
@ -151,9 +156,12 @@
|
|||
@import "./views/messages/_UnknownBody.scss";
|
||||
@import "./views/messages/_ViewSourceEvent.scss";
|
||||
@import "./views/messages/_common_CryptoEvent.scss";
|
||||
@import "./views/right_panel/_BaseCard.scss";
|
||||
@import "./views/right_panel/_EncryptionInfo.scss";
|
||||
@import "./views/right_panel/_RoomSummaryCard.scss";
|
||||
@import "./views/right_panel/_UserInfo.scss";
|
||||
@import "./views/right_panel/_VerificationPanel.scss";
|
||||
@import "./views/right_panel/_WidgetCard.scss";
|
||||
@import "./views/room_settings/_AliasSettings.scss";
|
||||
@import "./views/rooms/_AppsDrawer.scss";
|
||||
@import "./views/rooms/_Autocomplete.scss";
|
||||
|
@ -180,7 +188,6 @@
|
|||
@import "./views/rooms/_RoomHeader.scss";
|
||||
@import "./views/rooms/_RoomList.scss";
|
||||
@import "./views/rooms/_RoomPreviewBar.scss";
|
||||
@import "./views/rooms/_RoomRecoveryReminder.scss";
|
||||
@import "./views/rooms/_RoomSublist.scss";
|
||||
@import "./views/rooms/_RoomTile.scss";
|
||||
@import "./views/rooms/_RoomUpgradeWarningBar.scss";
|
||||
|
@ -195,10 +202,10 @@
|
|||
@import "./views/settings/_E2eAdvancedPanel.scss";
|
||||
@import "./views/settings/_EmailAddresses.scss";
|
||||
@import "./views/settings/_IntegrationManager.scss";
|
||||
@import "./views/settings/_KeyBackupPanel.scss";
|
||||
@import "./views/settings/_Notifications.scss";
|
||||
@import "./views/settings/_PhoneNumbers.scss";
|
||||
@import "./views/settings/_ProfileSettings.scss";
|
||||
@import "./views/settings/_SecureBackupPanel.scss";
|
||||
@import "./views/settings/_SetIdServer.scss";
|
||||
@import "./views/settings/_SetIntegrationManager.scss";
|
||||
@import "./views/settings/_UpdateCheckButton.scss";
|
||||
|
|
|
@ -22,7 +22,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_CustomRoomTagPanel {
|
||||
background-color: $tagpanel-bg-color;
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
|
|
|
@ -23,6 +23,13 @@ limitations under the License.
|
|||
|
||||
.mx_FilePanel .mx_RoomView_messageListWrapper {
|
||||
margin-right: 20px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_FilePanel .mx_RoomView_MessageList {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_FilePanel .mx_RoomView_MessageList h2 {
|
||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_TagPanel {
|
||||
.mx_GroupFilterPanel {
|
||||
flex: 1;
|
||||
background-color: $tagpanel-bg-color;
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
|
@ -26,63 +26,89 @@ limitations under the License.
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel_items_selected {
|
||||
.mx_GroupFilterPanel_items_selected {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_clearButton_container {
|
||||
/* Constant height within flex mx_TagPanel */
|
||||
height: 70px;
|
||||
width: 56px;
|
||||
|
||||
flex: none;
|
||||
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_clearButton object {
|
||||
/* Same as .mx_SearchBox padding-top */
|
||||
margin-top: 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_divider {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_divider {
|
||||
height: 0px;
|
||||
width: 34px;
|
||||
border-bottom: 1px solid $panel-divider-color;
|
||||
display: none;
|
||||
width: 90%;
|
||||
border: none;
|
||||
border-bottom: 1px solid $groupFilterPanel-divider-color;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_scroller {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_scroller {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_tagTileContainer {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding-top: 6px;
|
||||
}
|
||||
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile {
|
||||
.mx_GroupFilterPanel .mx_TagTile {
|
||||
// opacity: 0.5;
|
||||
position: relative;
|
||||
}
|
||||
.mx_TagPanel .mx_TagTile:focus,
|
||||
.mx_TagPanel .mx_TagTile:hover,
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected {
|
||||
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.mx_GroupFilterPanel .mx_TagTile:focus,
|
||||
.mx_GroupFilterPanel .mx_TagTile:hover,
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected {
|
||||
// opacity: 1;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile_plus {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype {
|
||||
background-color: $primary-bg-color;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.mx_TagTile_selected_prototype {
|
||||
.mx_TagTile_homeIcon::before {
|
||||
background-color: $primary-fg-color; // dark-on-light
|
||||
}
|
||||
}
|
||||
|
||||
.mx_TagTile:not(.mx_TagTile_selected_prototype) .mx_TagTile_homeIcon {
|
||||
background-color: $roomheader-addroom-bg-color;
|
||||
border-radius: 48px;
|
||||
|
||||
&::before {
|
||||
background-color: $roomheader-addroom-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_TagTile_homeIcon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/home.svg');
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 21px;
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
position: absolute;
|
||||
top: calc(50% - 16px);
|
||||
left: calc(50% - 16px);
|
||||
}
|
||||
}
|
||||
|
||||
.mx_GroupFilterPanel .mx_TagTile_plus {
|
||||
margin-bottom: 12px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
@ -106,7 +132,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
background-color: $accent-color;
|
||||
|
@ -116,7 +142,7 @@ limitations under the License.
|
|||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||
filter: none;
|
||||
}
|
||||
|
|
@ -18,6 +18,14 @@ limitations under the License.
|
|||
display: flex;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_buttons + .mx_HeaderButtons {
|
||||
// remove the | separator line for when next to RoomHeaderButtons
|
||||
// TODO: remove this once when we redo communities and make the right panel similar to the new rooms one
|
||||
&::before {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_HeaderButtons::before {
|
||||
content: "";
|
||||
background-color: $header-divider-color;
|
||||
|
|
|
@ -14,29 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
$tagPanelWidth: 56px; // only applies in this file, used for calculations
|
||||
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
||||
|
||||
.mx_LeftPanel {
|
||||
background-color: $roomlist-bg-color;
|
||||
min-width: 260px;
|
||||
max-width: 50%;
|
||||
|
||||
// Create a row-based flexbox for the TagPanel and the room list
|
||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||
display: flex;
|
||||
|
||||
.mx_LeftPanel_tagPanelContainer {
|
||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: $tagPanelWidth;
|
||||
flex-basis: $groupFilterPanelWidth;
|
||||
height: 100%;
|
||||
|
||||
// Create another flexbox so the TagPanel fills the container
|
||||
// Create another flexbox so the GroupFilterPanel fills the container
|
||||
display: flex;
|
||||
|
||||
// TagPanel handles its own CSS
|
||||
// GroupFilterPanel handles its own CSS
|
||||
}
|
||||
|
||||
&:not(.mx_LeftPanel_hasTagPanel) {
|
||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
|
|||
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: calc(100% - $tagPanelWidth);
|
||||
width: calc(100% - $groupFilterPanelWidth);
|
||||
background-color: $roomlist-bg-color;
|
||||
|
||||
// Create another flexbox (this time a column) for the room list components
|
||||
|
@ -169,10 +169,10 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
|
|||
min-width: unset;
|
||||
|
||||
// We have to forcefully set the width to override the resizer's style attribute.
|
||||
&.mx_LeftPanel_hasTagPanel {
|
||||
width: calc(68px + $tagPanelWidth) !important;
|
||||
&.mx_LeftPanel_hasGroupFilterPanel {
|
||||
width: calc(68px + $groupFilterPanelWidth) !important;
|
||||
}
|
||||
&:not(.mx_LeftPanel_hasTagPanel) {
|
||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||
width: 68px !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ limitations under the License.
|
|||
padding: 5px;
|
||||
// margin left to not allow the handle to not encroach on the space for the scrollbar
|
||||
margin-left: 8px;
|
||||
height: calc(100vh - 51px); // height of .mx_RoomHeader.light-panel
|
||||
|
||||
&:hover .mx_RightPanel_ResizeHandle {
|
||||
// Need to use important to override element style attributes
|
||||
|
|
|
@ -22,7 +22,13 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_NotificationPanel .mx_RoomView_messageListWrapper {
|
||||
margin-right: 20px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_RoomView_MessageList {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_RoomView_MessageList h2 {
|
||||
|
@ -35,11 +41,32 @@ limitations under the License.
|
|||
|
||||
.mx_NotificationPanel .mx_EventTile {
|
||||
word-break: break-word;
|
||||
position: relative;
|
||||
padding-bottom: 18px;
|
||||
|
||||
&:not(.mx_EventTile_last):not(.mx_EventTile_lastInSection)::after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: $tertiary-fg-color;
|
||||
height: 1px;
|
||||
opacity: 0.4;
|
||||
content: '';
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_roomName {
|
||||
font-weight: bold;
|
||||
font-size: $font-14px;
|
||||
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> .mx_BaseAvatar {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_roomName a {
|
||||
|
@ -47,8 +74,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_avatar {
|
||||
top: 8px;
|
||||
left: 0px;
|
||||
display: none; // we don't need this in this view
|
||||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile .mx_SenderProfile,
|
||||
|
@ -60,8 +86,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_NotificationPanel .mx_EventTile_senderDetails {
|
||||
padding-left: 32px;
|
||||
padding-top: 8px;
|
||||
padding-left: 36px; // align with the room name
|
||||
position: relative;
|
||||
|
||||
a {
|
||||
|
@ -82,7 +107,7 @@ limitations under the License.
|
|||
|
||||
.mx_NotificationPanel .mx_EventTile_line {
|
||||
margin-right: 0px;
|
||||
padding-left: 32px;
|
||||
padding-left: 36px; // align with the room name
|
||||
padding-top: 0px;
|
||||
padding-bottom: 0px;
|
||||
padding-right: 0px;
|
||||
|
|
|
@ -68,16 +68,14 @@ limitations under the License.
|
|||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RightPanel_membersButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
&:hover {
|
||||
background: rgba($accent-color, 0.1);
|
||||
|
||||
.mx_RightPanel_filesButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
||||
mask-position: center;
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RightPanel_notifsButton::before {
|
||||
|
@ -85,6 +83,11 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_roomSummaryButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
|
||||
mask-position: center;
|
||||
}
|
||||
|
||||
.mx_RightPanel_groupMembersButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||
mask-position: center;
|
||||
|
@ -96,23 +99,11 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RightPanel_headerButton_highlight {
|
||||
background: rgba($accent-color, 0.25);
|
||||
// make the icon the accent color too
|
||||
&::before {
|
||||
background-color: $accent-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RightPanel_headerButton:not(.mx_RightPanel_headerButton_highlight) {
|
||||
&:hover {
|
||||
background: rgba($accent-color, 0.1);
|
||||
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RightPanel_headerButton_badge {
|
||||
font-size: $font-8px;
|
||||
border-radius: 8px;
|
||||
|
@ -146,7 +137,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RightPanel_empty {
|
||||
margin-right: -42px;
|
||||
margin-right: -28px;
|
||||
|
||||
h2 {
|
||||
font-weight: 700;
|
||||
|
|
|
@ -133,6 +133,10 @@ limitations under the License.
|
|||
.mx_RoomDirectory_topic {
|
||||
cursor: initial;
|
||||
color: $light-fg-color;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_RoomDirectory_alias {
|
||||
|
|
|
@ -185,13 +185,11 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_RoomView_empty {
|
||||
flex: 1 1 auto;
|
||||
font-size: $font-13px;
|
||||
padding-left: 3em;
|
||||
padding-right: 3em;
|
||||
margin-right: 20px;
|
||||
margin-top: 33%;
|
||||
padding: 0 24px;
|
||||
margin-right: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 80px; // visually center the content (intentional offset)
|
||||
}
|
||||
|
||||
.mx_RoomView_MessageList {
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
.mx_TabbedView {
|
||||
margin: 0;
|
||||
padding: 0 0 0 58px;
|
||||
padding: 0 0 0 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
|
@ -25,6 +25,7 @@ limitations under the License.
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mx_TabbedView_tabLabels {
|
||||
|
@ -35,13 +36,13 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_TabbedView_tabLabel {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
vertical-align: text-top;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
font-size: $font-14px;
|
||||
min-height: 24px; // use min-height instead of height to allow the label to overflow a bit
|
||||
margin-bottom: 6px;
|
||||
padding: 8px 0;
|
||||
border-radius: 8px;
|
||||
font-size: $font-13px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -51,9 +52,8 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_TabbedView_maskedIcon {
|
||||
margin-left: 6px;
|
||||
margin-right: 9px;
|
||||
margin-top: 1px;
|
||||
margin-left: 8px;
|
||||
margin-right: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
|
@ -65,10 +65,9 @@ limitations under the License.
|
|||
mask-repeat: no-repeat;
|
||||
mask-size: 16px;
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
height: 16px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_TabbedView_tabLabel_active .mx_TabbedView_maskedIcon::before {
|
||||
|
|
|
@ -80,6 +80,11 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_Toast_icon_secure_backup::after {
|
||||
mask-image: url('$(res)/img/feather-customised/secure-backup.svg');
|
||||
background-color: $primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_Toast_title, .mx_Toast_body {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
|
|
@ -15,10 +15,33 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_UserMenu {
|
||||
|
||||
// to make the ... button sort of aligned with the explore button below
|
||||
// to make the menu button sort of aligned with the explore button below
|
||||
padding-right: 6px;
|
||||
|
||||
&.mx_UserMenu_prototype {
|
||||
// The margin & padding combination between here and the ::after is to
|
||||
// align the border line with the tag panel.
|
||||
margin-bottom: 6px;
|
||||
|
||||
padding-right: 0; // make the right edge line up with the explore button
|
||||
|
||||
.mx_UserMenu_headerButtons {
|
||||
// considering we've eliminated right padding on the menu itself, we need to
|
||||
// push the chevron in slightly (roughly lining up with the center of the
|
||||
// plus buttons)
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
// we cheat opacity on the theme colour with an after selector here
|
||||
&::after {
|
||||
content: '';
|
||||
border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse
|
||||
opacity: 0.2;
|
||||
display: block;
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_headerButtons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
@ -35,8 +58,8 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
background: $primary-fg-color;
|
||||
mask-image: url('$(res)/img/element-icons/context-menu.svg');
|
||||
background: $tertiary-fg-color;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,6 +79,28 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_doubleName {
|
||||
flex: 1;
|
||||
min-width: 0; // make flexbox aware that it can crush this to a tiny width
|
||||
|
||||
.mx_UserMenu_userName,
|
||||
.mx_UserMenu_subUserName {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_UserMenu_subUserName {
|
||||
color: $muted-fg-color;
|
||||
font-size: $font-13px;
|
||||
line-height: $font-18px;
|
||||
flex: 1;
|
||||
|
||||
// Ellipsize any text overflow
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserMenu_userName {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
|
@ -89,6 +134,44 @@ limitations under the License.
|
|||
.mx_UserMenu_contextMenu {
|
||||
width: 247px;
|
||||
|
||||
// These override the styles already present on the user menu rather than try to
|
||||
// define a new menu. They are specifically for the stacked menu when a community
|
||||
// is being represented as a prototype.
|
||||
&.mx_UserMenu_contextMenu_prototype {
|
||||
padding-bottom: 16px;
|
||||
|
||||
.mx_UserMenu_contextMenu_header {
|
||||
padding-bottom: 0;
|
||||
padding-top: 16px;
|
||||
|
||||
&:nth-child(n + 2) {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
hr {
|
||||
width: 85%;
|
||||
opacity: 0.2;
|
||||
border: none;
|
||||
border-bottom: 1px solid $primary-fg-color; // XXX: Variable abuse
|
||||
}
|
||||
|
||||
&.mx_IconizedContextMenu {
|
||||
> .mx_IconizedContextMenu_optionList {
|
||||
margin-top: 4px;
|
||||
|
||||
&::before {
|
||||
border: none;
|
||||
}
|
||||
|
||||
> .mx_AccessibleButton {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_IconizedContextMenu .mx_IconizedContextMenu_optionList_red {
|
||||
.mx_AccessibleButton {
|
||||
padding-top: 16px;
|
||||
|
@ -193,4 +276,12 @@ limitations under the License.
|
|||
.mx_UserMenu_iconSignOut::before {
|
||||
mask-image: url('$(res)/img/element-icons/leave.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconMembers::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/members.svg');
|
||||
}
|
||||
|
||||
.mx_UserMenu_iconInvite::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,12 @@ limitations under the License.
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
&.mx_WelcomePage_registrationDisabled {
|
||||
.mx_ButtonCreateAccount {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Welcome .mx_AuthBody_language {
|
||||
|
|
|
@ -82,7 +82,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
span.mx_IconizedContextMenu_label { // labels
|
||||
padding-left: 14px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
|
||||
|
@ -91,6 +90,10 @@ limitations under the License.
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu_icon + .mx_IconizedContextMenu_label {
|
||||
padding-left: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
88
res/css/views/dialogs/_CommunityPrototypeInviteDialog.scss
Normal file
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
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_CommunityPrototypeInviteDialog {
|
||||
&.mx_Dialog_fixedWidth {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 0;
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_people {
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
display: inline-block;
|
||||
background-color: $focus-bg-color; // XXX: Abuse of variables
|
||||
border-radius: 4px;
|
||||
padding: 3px 5px;
|
||||
font-size: $font-12px;
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_morePeople {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_person {
|
||||
position: relative;
|
||||
margin-top: 4px;
|
||||
|
||||
& > * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.mx_Checkbox {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(50% - 8px); // checkbox is 16px high
|
||||
width: 16px; // to force a square
|
||||
}
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_personIdentifiers {
|
||||
display: inline-block;
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_personName {
|
||||
font-weight: 600;
|
||||
font-size: $font-14px;
|
||||
color: $primary-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_personId {
|
||||
font-size: $font-12px;
|
||||
color: $muted-fg-color;
|
||||
margin-left: 7px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CommunityPrototypeInviteDialog_primaryButton {
|
||||
display: block;
|
||||
font-size: $font-13px;
|
||||
line-height: 20px;
|
||||
height: 20px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
102
res/css/views/dialogs/_CreateCommunityPrototypeDialog.scss
Normal file
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
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_CreateCommunityPrototypeDialog {
|
||||
.mx_Dialog_content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_colName {
|
||||
flex-basis: 66.66%;
|
||||
padding-right: 100px;
|
||||
|
||||
.mx_Field input {
|
||||
font-size: $font-16px;
|
||||
line-height: $font-20px;
|
||||
}
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_subtext {
|
||||
display: block;
|
||||
color: $muted-fg-color;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&:last-child {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
&.mx_CreateCommunityPrototypeDialog_subtext_error {
|
||||
color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_communityId {
|
||||
position: relative;
|
||||
|
||||
.mx_InfoTooltip {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
display: block;
|
||||
height: 32px;
|
||||
font-size: $font-16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_colAvatar {
|
||||
flex-basis: 33.33%;
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_avatarContainer {
|
||||
margin-top: 12px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_avatar,
|
||||
.mx_CreateCommunityPrototypeDialog_placeholderAvatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 96px;
|
||||
}
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_placeholderAvatar {
|
||||
background-color: #368bd6; // hardcoded for both themes
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
background-color: #fff; // hardcoded because the background is
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 96px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
mask-image: url('$(res)/img/element-icons/add-photo.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateCommunityPrototypeDialog_tip {
|
||||
& > b, & > span {
|
||||
display: block;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
77
res/css/views/dialogs/_EditCommunityPrototypeDialog.scss
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
// XXX: many of these styles are shared with the create dialog
|
||||
.mx_EditCommunityPrototypeDialog {
|
||||
&.mx_Dialog_fixedWidth {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
margin-bottom: 12px;
|
||||
|
||||
.mx_AccessibleButton.mx_AccessibleButton_kind_primary {
|
||||
display: block;
|
||||
height: 32px;
|
||||
font-size: $font-16px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_rowAvatar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_avatarContainer {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_avatar,
|
||||
.mx_EditCommunityPrototypeDialog_placeholderAvatar {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 96px;
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_placeholderAvatar {
|
||||
background-color: #368bd6; // hardcoded for both themes
|
||||
|
||||
&::before {
|
||||
display: inline-block;
|
||||
background-color: #fff; // hardcoded because the background is
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 96px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
mask-image: url('$(res)/img/element-icons/add-photo.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_EditCommunityPrototypeDialog_tip {
|
||||
margin-left: 20px;
|
||||
|
||||
& > b, & > span {
|
||||
display: block;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -89,6 +89,13 @@ limitations under the License.
|
|||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_subname {
|
||||
margin-bottom: 10px;
|
||||
margin-top: -10px; // HACK: Positioning with margins is bad
|
||||
font-size: $font-12px;
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_roomTile {
|
||||
|
@ -226,3 +233,7 @@ limitations under the License.
|
|||
.mx_InviteDialog_addressBar {
|
||||
margin-right: 45px;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_helpText .mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,6 @@ limitations under the License.
|
|||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
margin: 0 auto;
|
||||
padding-left: 40px;
|
||||
padding-right: 80px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,50 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations 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_SetMxIdDialog .mx_Dialog_title {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.mx_SetMxIdDialog_input_group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.mx_SetMxIdDialog_input {
|
||||
border-radius: 3px;
|
||||
border: 1px solid $input-border-color;
|
||||
padding: 9px;
|
||||
color: $primary-fg-color;
|
||||
background-color: $primary-bg-color;
|
||||
font-size: $font-15px;
|
||||
width: 100%;
|
||||
max-width: 280px;
|
||||
}
|
||||
|
||||
.mx_SetMxIdDialog_input.error,
|
||||
.mx_SetMxIdDialog_input.error:focus {
|
||||
border: 1px solid $warning-color;
|
||||
}
|
||||
|
||||
.mx_SetMxIdDialog_input_group .mx_Spinner {
|
||||
height: 37px;
|
||||
padding-left: 10px;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.mx_SetMxIdDialog .success {
|
||||
color: $accent-color;
|
||||
}
|
|
@ -36,7 +36,6 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_Dialog_title {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,9 +71,12 @@ limitations under the License.
|
|||
margin-right: 64px;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_qrcode_container + .mx_ShareDialog_social_container {
|
||||
width: 299px;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_social_container {
|
||||
display: inline-block;
|
||||
width: 299px;
|
||||
}
|
||||
|
||||
.mx_ShareDialog_social_icon {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
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_CreateCrossSigningDialog {
|
||||
// Why you ask? Because CompleteSecurityBody is 600px so this is the width
|
||||
// we end up when in there, but when in our own dialog we set our own width
|
||||
// so need to fix it to something sensible as otherwise we'd end up either
|
||||
// really wide or really narrow depending on the phase. I bet you wish you
|
||||
// never asked.
|
||||
width: 560px;
|
||||
|
||||
details .mx_AccessibleButton {
|
||||
margin: 1em 0; // emulate paragraph spacing because we can't put this button in a paragraph due to HTML rules
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CreateCrossSigningDialog .mx_Dialog_title {
|
||||
/* TODO: Consider setting this for all dialog titles. */
|
||||
margin-bottom: 1em;
|
||||
}
|
|
@ -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.
|
||||
|
@ -14,13 +14,15 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
.mx_DesktopBuildsNotice {
|
||||
text-align: center;
|
||||
padding: 0 16px;
|
||||
|
||||
export default function(dest, src) {
|
||||
for (const i in src) {
|
||||
if (src.hasOwnProperty(i)) {
|
||||
dest[i] = src[i];
|
||||
> * {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
> img {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2017 Vector Creations 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.
|
||||
|
@ -14,21 +14,21 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SetPasswordDialog_change_password input {
|
||||
border-radius: 3px;
|
||||
border: 1px solid $input-border-color;
|
||||
padding: 9px;
|
||||
color: $primary-fg-color;
|
||||
background-color: $primary-bg-color;
|
||||
font-size: $font-15px;
|
||||
max-width: 280px;
|
||||
margin-bottom: 10px;
|
||||
.mx_InfoTooltip_icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_SetPasswordDialog_change_password_button {
|
||||
margin-top: 68px;
|
||||
}
|
||||
|
||||
.mx_SetPasswordDialog .mx_Dialog_content {
|
||||
margin-bottom: 0px;
|
||||
.mx_InfoTooltip_icon::before {
|
||||
display: inline-block;
|
||||
background-color: $muted-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
mask-position: center;
|
||||
content: '';
|
||||
vertical-align: middle;
|
||||
mask-image: url('$(res)/img/element-icons/info.svg');
|
||||
}
|
55
res/css/views/messages/_MJitsiWidgetEvent.scss
Normal file
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_MJitsiWidgetEvent {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||
|
||||
&::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
||||
margin-top: 4px;
|
||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_title {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_subtitle {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_title,
|
||||
.mx_MJitsiWidgetEvent_subtitle {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
165
res/css/views/right_panel/_BaseCard.scss
Normal file
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
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_BaseCard {
|
||||
padding: 0 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
.mx_BaseCard_header {
|
||||
margin: 8px 0;
|
||||
|
||||
> h2 {
|
||||
margin: 0 44px;
|
||||
font-size: $font-18px;
|
||||
font-weight: $font-semi-bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mx_BaseCard_back, .mx_BaseCard_close {
|
||||
position: absolute;
|
||||
background-color: rgba(141, 151, 165, 0.2);
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
margin: 12px;
|
||||
top: 0;
|
||||
border-radius: 10px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
top: 0;
|
||||
left: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $rightpanel-button-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BaseCard_back {
|
||||
left: 0;
|
||||
|
||||
&::before {
|
||||
transform: rotate(90deg);
|
||||
mask-size: 22px;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BaseCard_close {
|
||||
right: 0;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/icons-close.svg');
|
||||
mask-size: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AutoHideScrollbar {
|
||||
// collapse the margin into a padding to move the scrollbar into the right gutter
|
||||
margin-right: -8px;
|
||||
padding-right: 8px;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_BaseCard_Group {
|
||||
margin: 20px 0 16px;
|
||||
|
||||
& > * {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
> h1 {
|
||||
color: $tertiary-fg-color;
|
||||
font-size: $font-12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mx_BaseCard_Button {
|
||||
padding: 10px 38px 10px 12px;
|
||||
margin: 0;
|
||||
position: relative;
|
||||
font-size: $font-13px;
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(141, 151, 165, 0.1);
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 6px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $icon-button-color;
|
||||
transform: rotate(270deg);
|
||||
mask-size: 20px;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_BaseCard_footer {
|
||||
padding-top: 4px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
|
||||
.mx_AccessibleButton_kind_secondary {
|
||||
color: $secondary-fg-color;
|
||||
background-color: rgba(141, 151, 165, 0.2);
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-14px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_FilePanel,
|
||||
.mx_UserInfo,
|
||||
.mx_NotificationPanel,
|
||||
.mx_MemberList {
|
||||
&.mx_BaseCard {
|
||||
padding: 32px 0 0;
|
||||
|
||||
.mx_AutoHideScrollbar {
|
||||
margin-right: unset;
|
||||
padding-right: unset;
|
||||
}
|
||||
}
|
||||
}
|
161
res/css/views/right_panel/_RoomSummaryCard.scss
Normal file
|
@ -0,0 +1,161 @@
|
|||
/*
|
||||
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_RoomSummaryCard {
|
||||
.mx_BaseCard_header {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
|
||||
h2 {
|
||||
font-weight: $font-semi-bold;
|
||||
font-size: $font-18px;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_alias {
|
||||
font-size: $font-13px;
|
||||
color: $secondary-fg-color;
|
||||
}
|
||||
|
||||
h2, .mx_RoomSummaryCard_alias {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_avatar {
|
||||
display: inline-flex;
|
||||
|
||||
.mx_RoomSummaryCard_e2ee {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background-color: #737d8c;
|
||||
margin-top: -3px; // alignment
|
||||
margin-left: -10px; // overlap
|
||||
border: 3px solid $dark-panel-bg-color;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 13px;
|
||||
left: 13px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
mask-size: cover;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-image: url('$(res)/img/e2e/disabled.svg');
|
||||
background-color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_e2ee_normal {
|
||||
background-color: #424446;
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_e2ee_verified {
|
||||
background-color: #0dbd8b;
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/e2e/verified.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_e2ee_warning {
|
||||
background-color: #ff4b55;
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/e2e/warning.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_aboutGroup {
|
||||
.mx_RoomSummaryCard_Button {
|
||||
padding-left: 44px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
background-color: $icon-button-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_appsGroup {
|
||||
.mx_RoomSummaryCard_Button {
|
||||
padding-left: 12px;
|
||||
color: $tertiary-fg-color;
|
||||
|
||||
span {
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
margin-right: 12px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_app_pinned::after {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||
background-color: $accent-color;
|
||||
transform: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_link {
|
||||
padding: 0;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-size: $font-13px;
|
||||
font-weight: $font-semi-bold;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_people::before {
|
||||
mask-image: url("$(res)/img/element-icons/room/members.svg");
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_files::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/files.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_share::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/share.svg');
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_settings::before {
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
|
@ -15,7 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_UserInfo {
|
||||
.mx_UserInfo.mx_BaseCard {
|
||||
// UserInfo has a circular image at the top so it fits between the back & close buttons
|
||||
padding-top: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
@ -217,9 +219,8 @@ limitations under the License.
|
|||
text-overflow: clip;
|
||||
}
|
||||
|
||||
.mx_UserInfo_scrollContainer {
|
||||
.mx_AutoHideScrollbar {
|
||||
flex: 1 1 0;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.mx_UserInfo_container:not(.mx_UserInfo_separator) {
|
||||
|
|
62
res/css/views/right_panel/_WidgetCard.scss
Normal file
|
@ -0,0 +1,62 @@
|
|||
/*
|
||||
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_WidgetCard {
|
||||
height: 100%;
|
||||
display: contents;
|
||||
|
||||
.mx_AppTileFullWidth {
|
||||
max-width: unset;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
&.mx_WidgetCard_noEdit {
|
||||
.mx_AccessibleButton_kind_secondary {
|
||||
margin: 0 12px;
|
||||
|
||||
&:first-child {
|
||||
// expand the Pin to room primary action
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_WidgetCard_optionsButton {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
width: 26px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 6px;
|
||||
left: 20px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
background-color: $secondary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_WidgetCard_maxPinnedTooltip {
|
||||
background-color: $notice-primary-color;
|
||||
color: #ffffff;
|
||||
}
|
|
@ -15,90 +15,111 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
the tile title bar is 5 (top border) + 12 (title, buttons) + 5 (bottom padding) px = 22px
|
||||
the body is assumed to be 300px (assumed by at least the sticker pickerm, perhaps elsewhere),
|
||||
so the body height would be 300px - 22px (room for title bar) = 278px
|
||||
BUT! the sticker picker also assumes it's a little less high than that because the iframe
|
||||
for the sticker picker doesn't have any padding or margin on it's bottom.
|
||||
so subtracking another 5px, which brings us at 273px.
|
||||
*/
|
||||
$AppsDrawerBodyHeight: 273px;
|
||||
$MiniAppTileHeight: 200px;
|
||||
|
||||
.mx_AppsDrawer {
|
||||
margin: 5px;
|
||||
margin: 5px 5px 5px 18px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.mx_AppsContainer_resizerHandle {
|
||||
cursor: ns-resize;
|
||||
border-radius: 3px;
|
||||
|
||||
// Override styles from library
|
||||
width: unset !important;
|
||||
height: 4px !important;
|
||||
|
||||
// This is positioned directly below frame
|
||||
position: absolute;
|
||||
bottom: -8px !important; // override from library
|
||||
|
||||
// Together, these make the bar 64px wide
|
||||
// These are also overridden from the library
|
||||
left: calc(50% - 32px) !important;
|
||||
right: calc(50% - 32px) !important;
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_hidden {
|
||||
display: none;
|
||||
&:hover {
|
||||
.mx_AppsContainer_resizerHandle {
|
||||
opacity: 0.8;
|
||||
background: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppsContainer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-items: stretch;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_minimised .mx_AppsContainer {
|
||||
// override the re-resizable inline styles
|
||||
height: inherit !important;
|
||||
min-height: inherit !important;
|
||||
}
|
||||
|
||||
.mx_AddWidget_button {
|
||||
order: 2;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: 5px auto 5px auto;
|
||||
margin: -3px auto 5px 0;
|
||||
color: $accent-color;
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
||||
.mx_AddWidget_button_full_width {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.mx_SetAppURLDialog_input {
|
||||
border-radius: 3px;
|
||||
border: 1px solid $input-border-color;
|
||||
padding: 9px;
|
||||
color: $primary-hairline-color;
|
||||
background-color: $primary-bg-color;
|
||||
font-size: $font-15px;
|
||||
}
|
||||
|
||||
.mx_AppTile {
|
||||
max-width: 960px;
|
||||
width: 50%;
|
||||
margin-right: 5px;
|
||||
border: 5px solid $widget-menu-bar-bg-color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.mx_AppTile:last-child {
|
||||
margin-right: 1px;
|
||||
& + .mx_AppTile {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppTileFullWidth {
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 5px solid $widget-menu-bar-bg-color;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mx_AppTile_mini {
|
||||
max-width: 960px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: $MiniAppTileHeight;
|
||||
}
|
||||
|
||||
.mx_AppTile_persistedWrapper {
|
||||
height: $AppsDrawerBodyHeight;
|
||||
.mx_AppTile.mx_AppTile_minimised,
|
||||
.mx_AppTileFullWidth.mx_AppTile_minimised,
|
||||
.mx_AppTile_mini.mx_AppTile_minimised {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.mx_AppTile .mx_AppTile_persistedWrapper,
|
||||
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||
height: 114px;
|
||||
min-width: 300px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_AppTile_persistedWrapper div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar {
|
||||
|
@ -110,6 +131,7 @@ $AppsDrawerBodyHeight: 273px;
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_expanded {
|
||||
|
@ -172,15 +194,23 @@ $AppsDrawerBodyHeight: 273px;
|
|||
}
|
||||
|
||||
.mx_AppTileBody {
|
||||
height: $AppsDrawerBodyHeight;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_AppTileBody_mini {
|
||||
height: 112px;
|
||||
height: $MiniAppTileHeight;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_AppTile .mx_AppTileBody,
|
||||
.mx_AppTileFullWidth .mx_AppTileBody,
|
||||
.mx_AppTile_mini .mx_AppTileBody_mini {
|
||||
height: inherit;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_AppTileBody_mini iframe {
|
||||
|
@ -191,7 +221,7 @@ $AppsDrawerBodyHeight: 273px;
|
|||
|
||||
.mx_AppTileBody iframe {
|
||||
width: 100%;
|
||||
height: $AppsDrawerBodyHeight;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
padding: 0;
|
||||
|
@ -199,72 +229,6 @@ $AppsDrawerBodyHeight: 273px;
|
|||
display: block;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBarWidgetPadding {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mx_AppIconTile {
|
||||
background-color: $lightbox-bg-color;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
width: 200px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_AppIconTile.mx_AppIconTile_active {
|
||||
color: $accent-color;
|
||||
border-color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_AppIconTile:hover {
|
||||
border: 1px solid $accent-color;
|
||||
box-shadow: 0 0 10px 5px rgba(200, 200, 200, 0.5);
|
||||
}
|
||||
|
||||
.mx_AppIconTile_content {
|
||||
padding: 2px 16px;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_content h4 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_content p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_image {
|
||||
padding: 10px;
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_imageContainer {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border-radius: 3px 3px 0 0;
|
||||
height: 155px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form.mx_Custom_Widget_Form div {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_AppPermissionWarning {
|
||||
text-align: center;
|
||||
background-color: $widget-menu-bar-bg-color;
|
||||
|
@ -331,7 +295,7 @@ form.mx_Custom_Widget_Form div {
|
|||
align-items: center;
|
||||
font-weight: bold;
|
||||
position: relative;
|
||||
height: $AppsDrawerBodyHeight;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_AppLoading .mx_Spinner {
|
||||
|
@ -358,3 +322,16 @@ form.mx_Custom_Widget_Form div {
|
|||
.mx_AppLoading iframe {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_minimised .mx_AppsContainer_resizerHandle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Avoid apptile iframes capturing mouse event focus when resizing */
|
||||
.mx_AppsDrawer_resizing iframe {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
|
|
|
@ -394,16 +394,6 @@ $left-gutter: 64px;
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
.mx_EventTile_e2eIcon_hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* always override hidden attribute for blocked and warning */
|
||||
.mx_EventTile_e2eIcon_hidden[src*="img/e2e-blocked.svg"],
|
||||
.mx_EventTile_e2eIcon_hidden[src*="img/e2e-warning.svg"] {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_EventTile_keyRequestInfo {
|
||||
font-size: $font-12px;
|
||||
}
|
||||
|
|
|
@ -70,7 +70,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_MemberInfo_avatar {
|
||||
background: $tagpanel-bg-color;
|
||||
background: $groupFilterPanel-bg-color;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -70,6 +70,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_MemberList_query {
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.mx_MemberList_wrapper {
|
||||
padding: 10px;
|
||||
}
|
||||
|
@ -92,11 +96,21 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_MemberList_invite span {
|
||||
background-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
background-repeat: no-repeat;
|
||||
background-position: center left;
|
||||
background-size: 20px;
|
||||
padding: 8px 0 8px 25px;
|
||||
padding: 8px 0;
|
||||
display: inline-flex;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
background-color: $button-fg-color;
|
||||
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||
mask-position: center;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_MemberList_inviteCommunity span {
|
||||
|
|
|
@ -217,7 +217,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
&.mx_MessageComposer_hangup::before {
|
||||
&.mx_MessageComposer_hangup:not(.mx_AccessibleButton_disabled)::before {
|
||||
background-color: $warning-color;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -236,10 +236,6 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_RoomHeader_settingsButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/settings.svg');
|
||||
}
|
||||
|
||||
.mx_RoomHeader_forgetButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/leave.svg');
|
||||
width: 26px;
|
||||
|
@ -249,14 +245,6 @@ limitations under the License.
|
|||
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
||||
}
|
||||
|
||||
.mx_RoomHeader_shareButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/share.svg');
|
||||
}
|
||||
|
||||
.mx_RoomHeader_manageIntegsButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/integrations.svg');
|
||||
}
|
||||
|
||||
.mx_RoomHeader_showPanel {
|
||||
height: 16px;
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector 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_RoomRecoveryReminder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
background-color: $room-warning-bg-color;
|
||||
padding: 20px;
|
||||
border: 1px solid $primary-hairline-color;
|
||||
border-bottom: unset;
|
||||
}
|
||||
|
||||
.mx_RoomRecoveryReminder_header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_RoomRecoveryReminder_body {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.mx_RoomRecoveryReminder_secondary {
|
||||
font-size: 90%;
|
||||
margin-top: 1em;
|
||||
}
|
|
@ -68,3 +68,4 @@ limitations under the License.
|
|||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -7,7 +7,8 @@
|
|||
height: 300px;
|
||||
}
|
||||
|
||||
#mx_persistedElement_stickerPicker .mx_AppTileFullWidth {
|
||||
#mx_persistedElement_stickerPicker {
|
||||
.mx_AppTileFullWidth {
|
||||
height: unset;
|
||||
box-sizing: border-box;
|
||||
border-left: none;
|
||||
|
@ -15,6 +16,12 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
iframe {
|
||||
// Sticker picker depends on the fixed height previously used for all tiles
|
||||
height: 273px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Stickers_contentPlaceholder {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,13 +15,56 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_AvatarSetting_avatar {
|
||||
width: $font-88px;
|
||||
height: $font-88px;
|
||||
margin-left: 13px;
|
||||
width: 90px;
|
||||
min-width: 90px; // so it doesn't get crushed by the flexbox in languages with longer words
|
||||
height: 90px;
|
||||
margin-top: 8px;
|
||||
position: relative;
|
||||
|
||||
.mx_AvatarSetting_hover {
|
||||
transition: opacity $hover-transition;
|
||||
|
||||
// position to place the hover bg over the entire thing
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
pointer-events: none; // let the pointer fall through the underlying thing
|
||||
|
||||
line-height: 90px;
|
||||
text-align: center;
|
||||
|
||||
> span {
|
||||
color: #fff; // hardcoded to contrast with background
|
||||
position: relative; // tricks the layout engine into putting this on top of the bg
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_hoverBg {
|
||||
// absolute position to lazily fill the entire container
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
opacity: 0.5;
|
||||
background-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
border-radius: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_AvatarSetting_avatar_hovering .mx_AvatarSetting_hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:not(.mx_AvatarSetting_avatar_hovering) .mx_AvatarSetting_hover {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: $font-88px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
@ -30,7 +73,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_AccessibleButton.mx_AccessibleButton_kind_link_sm {
|
||||
color: $button-danger-bg-color;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
& > img {
|
||||
|
@ -41,8 +84,10 @@ limitations under the License.
|
|||
& > img,
|
||||
.mx_AvatarSetting_avatarPlaceholder {
|
||||
display: block;
|
||||
height: $font-88px;
|
||||
border-radius: 4px;
|
||||
height: 90px;
|
||||
width: inherit;
|
||||
border-radius: 90px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_avatarPlaceholder::before {
|
||||
|
@ -58,6 +103,29 @@ limitations under the License.
|
|||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_uploadButton {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
background-color: $settings-profile-button-bg-color;
|
||||
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_uploadButton::before {
|
||||
content: "";
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 55%;
|
||||
background-color: $settings-profile-button-fg-color;
|
||||
mask-image: url('$(res)/img/feather-customised/edit.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AvatarSetting_avatar .mx_AvatarSetting_avatarPlaceholder {
|
||||
|
|
|
@ -28,4 +28,8 @@ limitations under the License.
|
|||
|
||||
.mx_CrossSigningPanel_buttonRow {
|
||||
margin: 1em 0;
|
||||
|
||||
:nth-child(n + 1) {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -20,6 +20,13 @@ limitations under the License.
|
|||
|
||||
.mx_ProfileSettings_controls {
|
||||
flex-grow: 1;
|
||||
margin-right: 54px;
|
||||
|
||||
// We put the header under the controls with some minor styling to cheat
|
||||
// alignment of the field with the avatar
|
||||
.mx_SettingsTab_subheading {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_controls .mx_Field #profileTopic {
|
||||
|
@ -41,3 +48,17 @@ limitations under the License.
|
|||
.mx_ProfileSettings_avatarUpload {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_profileForm {
|
||||
@mixin mx_Settings_fullWidthField;
|
||||
border-bottom: 1px solid $menu-border-color;
|
||||
}
|
||||
|
||||
.mx_ProfileSettings_buttons {
|
||||
margin-top: 10px; // 18px is already accounted for by the <p> above the buttons
|
||||
margin-bottom: 28px;
|
||||
|
||||
> .mx_AccessibleButton_kind_link {
|
||||
padding-left: 0; // to align with left side
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,23 +15,39 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_sigInvalid,
|
||||
.mx_KeyBackupPanel_deviceVerified, .mx_KeyBackupPanel_deviceNotVerified {
|
||||
.mx_SecureBackupPanel_sigValid, .mx_SecureBackupPanel_sigInvalid,
|
||||
.mx_SecureBackupPanel_deviceVerified, .mx_SecureBackupPanel_deviceNotVerified {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mx_KeyBackupPanel_sigValid, .mx_KeyBackupPanel_deviceVerified {
|
||||
.mx_SecureBackupPanel_sigValid, .mx_SecureBackupPanel_deviceVerified {
|
||||
color: $e2e-verified-color;
|
||||
}
|
||||
|
||||
.mx_KeyBackupPanel_sigInvalid, .mx_KeyBackupPanel_deviceNotVerified {
|
||||
.mx_SecureBackupPanel_sigInvalid, .mx_SecureBackupPanel_deviceNotVerified {
|
||||
color: $e2e-warning-color;
|
||||
}
|
||||
|
||||
.mx_KeyBackupPanel_deviceName {
|
||||
.mx_SecureBackupPanel_deviceName {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.mx_KeyBackupPanel_buttonRow {
|
||||
.mx_SecureBackupPanel_buttonRow {
|
||||
margin: 1em 0;
|
||||
|
||||
:nth-child(n + 1) {
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_SecureBackupPanel_statusList {
|
||||
border-spacing: 0;
|
||||
|
||||
td {
|
||||
padding: 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-inline-end: 1em;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,6 +14,10 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SettingsTab {
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
|
||||
.mx_SettingsTab_warningText {
|
||||
color: $warning-color;
|
||||
}
|
||||
|
@ -22,6 +26,7 @@ limitations under the License.
|
|||
font-size: $font-20px;
|
||||
font-weight: 600;
|
||||
color: $primary-fg-color;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_SettingsTab_heading:nth-child(n + 2) {
|
||||
|
|
|
@ -22,6 +22,13 @@ limitations under the License.
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
// TODO: Make this selector less painful
|
||||
.mx_GeneralUserSettingsTab_accountSection .mx_SettingsTab_subheading:nth-child(n + 1),
|
||||
.mx_GeneralUserSettingsTab_discovery .mx_SettingsTab_subheading:nth-child(n + 2),
|
||||
.mx_SetIdServer .mx_SettingsTab_subheading {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mx_GeneralUserSettingsTab_accountSection .mx_Spinner,
|
||||
.mx_GeneralUserSettingsTab_discovery .mx_Spinner {
|
||||
// Move the spinner to the left side of the container (default center)
|
||||
|
|
|
@ -23,9 +23,16 @@ limitations under the License.
|
|||
z-index: 100;
|
||||
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
|
||||
|
||||
cursor: pointer;
|
||||
// Disable pointer events for Jitsi widgets to function. Direct
|
||||
// calls have their own cursor and behaviour, but we need to make
|
||||
// sure the cursor hits the iframe for Jitsi which will be at a
|
||||
// different level.
|
||||
pointer-events: none;
|
||||
|
||||
.mx_CallPreview {
|
||||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||
cursor: pointer;
|
||||
|
||||
.mx_VideoView {
|
||||
width: 350px;
|
||||
}
|
||||
|
@ -36,16 +43,23 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_AppTile_persistedWrapper div {
|
||||
min-width: 350px;
|
||||
}
|
||||
|
||||
.mx_IncomingCallBox {
|
||||
min-width: 250px;
|
||||
background-color: $primary-bg-color;
|
||||
padding: 8px;
|
||||
|
||||
pointer-events: initial; // restore pointer events so the user can accept/decline
|
||||
cursor: pointer;
|
||||
|
||||
.mx_IncomingCallBox_CallerInfo {
|
||||
display: flex;
|
||||
direction: row;
|
||||
|
||||
img {
|
||||
img, .mx_BaseAvatar_initial {
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
|
|
5
res/img/e2e/disabled.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.38 12.27C15.76 11.42 16 10.43 16 9.27V3.05L8.99997 1L5.21997 2.11L15.38 12.27Z" fill="#010101"/>
|
||||
<path d="M2.21 2.98999L2 3.04999V9.26999C2 15.63 9 17 9 17C9 17 11.71 16.47 13.76 14.53L2.21 2.98999Z" fill="#010101"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.46967 0.46967C0.762563 0.176777 1.23744 0.176777 1.53033 0.46967L16.7203 15.6597C17.0132 15.9526 17.0132 16.4274 16.7203 16.7203C16.4274 17.0132 15.9526 17.0132 15.6597 16.7203L0.46967 1.53033C0.176777 1.23744 0.176777 0.762563 0.46967 0.46967Z" fill="#010101"/>
|
||||
</svg>
|
After Width: | Height: | Size: 648 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M1.77777 8.24003V2.71114L7.99999 0.888916L14.2222 2.71114V8.24003C14.2222 13.8934 7.99999 15.1111 7.99999 15.1111C7.99999 15.1111 1.77777 13.8934 1.77777 8.24003Z" fill="#020202"/>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27Z" fill="#020202"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 293 B After Width: | Height: | Size: 204 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.77783 2.71114V8.24003C1.77783 13.8934 8.00005 15.1111 8.00005 15.1111C8.00005 15.1111 14.2223 13.8934 14.2223 8.24003V2.71114L8.00005 0.888916L1.77783 2.71114ZM10.6139 4.90635C10.7827 4.74635 11.0494 4.75524 11.2094 4.92413C11.3516 5.08413 11.3516 5.32413 11.2272 5.48413L7.47608 10.0263L7.44941 10.0619C7.20052 10.3641 6.74719 10.4086 6.44497 10.1597C6.41812 10.1463 6.39635 10.1227 6.37581 10.1005C6.36914 10.0933 6.36261 10.0862 6.35608 10.0797L4.74719 8.23079C4.56941 8.01746 4.58719 7.69746 4.80052 7.51968C4.9783 7.35968 5.23608 7.35968 5.42274 7.48413L6.8183 8.46191L10.6139 4.90635Z" fill="#0DBD8B"/>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3.05V9.27C2 15.63 9 17 9 17C9 17 16 15.63 16 9.27V3.05L9 1L2 3.05ZM11.9405 5.5196C12.1305 5.3396 12.4305 5.3496 12.6105 5.5396C12.7705 5.7196 12.7705 5.9896 12.6305 6.1696L8.41047 11.2796L8.38047 11.3196C8.10047 11.6596 7.59047 11.7096 7.25047 11.4296C7.22027 11.4145 7.19577 11.388 7.17266 11.363C7.16517 11.3549 7.15782 11.347 7.15047 11.3396L5.34047 9.2596C5.14047 9.0196 5.16047 8.6596 5.40047 8.4596C5.60047 8.2796 5.89047 8.2796 6.10047 8.4196L7.67047 9.5196L11.9405 5.5196Z" fill="#010101"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 764 B After Width: | Height: | Size: 658 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.77783 2.71114V8.24003C1.77783 13.8934 8.00005 15.1111 8.00005 15.1111C8.00005 15.1111 14.2223 13.8934 14.2223 8.24003V2.71114L8.00005 0.888916L1.77783 2.71114ZM7.92899 3.91112C7.42232 3.94668 7.04899 4.39113 7.09343 4.89779L7.37788 8.45335C7.40455 8.76446 7.64455 9.00446 7.95566 9.03113H8.00899C8.33788 9.03113 8.61343 8.78224 8.6401 8.45335L8.92455 4.89779V4.75557C8.87121 4.2489 8.42677 3.87557 7.92899 3.91112ZM8 11.5556C8.43201 11.5556 8.78222 11.2054 8.78222 10.7733C8.78222 10.3413 8.43201 9.99112 8 9.99112C7.56799 9.99112 7.21777 10.3413 7.21777 10.7733C7.21777 11.2054 7.56799 11.5556 8 11.5556Z" fill="#FF4B55"/>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 9.27V3.05L9 1L16 3.05V9.27C16 15.63 9 17 9 17C9 17 2 15.63 2 9.27ZM8.92011 4.39997C8.35011 4.43997 7.93011 4.93997 7.98011 5.50997L8.30011 9.50997C8.33011 9.85997 8.60011 10.13 8.95011 10.16H9.01011C9.38011 10.16 9.69011 9.87997 9.72011 9.50997L10.0401 5.50997V5.34997C9.98011 4.77997 9.48011 4.35997 8.92011 4.39997ZM9.88012 12.12C9.88012 12.606 9.48613 13 9.00012 13C8.51411 13 8.12012 12.606 8.12012 12.12C8.12012 11.634 8.51411 11.24 9.00012 11.24C9.48613 11.24 9.88012 11.634 9.88012 12.12Z" fill="#020202"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 779 B After Width: | Height: | Size: 673 B |
157
res/img/element-desktop-logo.svg
Normal file
|
@ -0,0 +1,157 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.1438 2.34375H7.85617C5.93938 2.34375 5.24431 2.54333 4.54356 2.91809C3.84281 3.29286 3.29286 3.84281 2.91809 4.54356C2.54333 5.24431 2.34375 5.93938 2.34375 7.85617V16.1438C2.34375 18.0606 2.54333 18.7557 2.91809 19.4564C3.29286 20.1572 3.84281 20.7071 4.54356 21.0819C5.24431 21.4567 5.93938 21.6562 7.85617 21.6562H16.1438C18.0606 21.6562 18.7557 21.4567 19.4564 21.0819C20.1572 20.7071 20.7071 20.1572 21.0819 19.4564C21.4567 18.7557 21.6562 18.0606 21.6562 16.1438V7.85617C21.6562 5.93938 21.4567 5.24431 21.0819 4.54356C20.7071 3.84281 20.1572 3.29286 19.4564 2.91809C18.7557 2.54333 18.0606 2.34375 16.1438 2.34375Z" fill="url(#paint0_linear)"/>
|
||||
</g>
|
||||
<g filter="url(#filter1_ddddi)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0969 6.01875C10.0969 5.56829 10.462 5.20312 10.9125 5.20312C13.9155 5.20312 16.35 7.63758 16.35 10.6406C16.35 11.0911 15.9848 11.4562 15.5344 11.4562C15.0839 11.4562 14.7187 11.0911 14.7187 10.6406C14.7187 8.53849 13.0146 6.83437 10.9125 6.83437C10.462 6.83437 10.0969 6.46921 10.0969 6.01875Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter2_ddddi)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.9031 17.9813C13.9031 18.4317 13.538 18.7969 13.0875 18.7969C10.0845 18.7969 7.65001 16.3624 7.65001 13.3594C7.65001 12.9089 8.01518 12.5437 8.46564 12.5437C8.9161 12.5437 9.28126 12.9089 9.28126 13.3594C9.28126 15.4615 10.9854 17.1656 13.0875 17.1656C13.538 17.1656 13.9031 17.5308 13.9031 17.9813Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter3_ddddi)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.01875 13.9031C5.56829 13.9031 5.20312 13.538 5.20312 13.0875C5.20312 10.0845 7.63758 7.65001 10.6406 7.65001C11.0911 7.65001 11.4562 8.01518 11.4562 8.46564C11.4562 8.91609 11.0911 9.28126 10.6406 9.28126C8.53849 9.28126 6.83437 10.9854 6.83437 13.0875C6.83437 13.538 6.46921 13.9031 6.01875 13.9031Z" fill="white"/>
|
||||
</g>
|
||||
<g filter="url(#filter4_ddddi)">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.9812 10.0969C18.4317 10.0969 18.7969 10.462 18.7969 10.9125C18.7969 13.9155 16.3624 16.35 13.3594 16.35C12.9089 16.35 12.5437 15.9848 12.5437 15.5344C12.5437 15.0839 12.9089 14.7187 13.3594 14.7187C15.4615 14.7187 17.1656 13.0146 17.1656 10.9125C17.1656 10.462 17.5308 10.0969 17.9812 10.0969Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="1.54688" y="1.92188" width="20.9062" height="20.9062" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="0.375"/>
|
||||
<feGaussianBlur stdDeviation="0.398438"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.09 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="0.234375"/>
|
||||
<feGaussianBlur stdDeviation="0.28125"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="filter1_ddddi" x="6.95624" y="4.03125" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="0.328125"/>
|
||||
<feGaussianBlur stdDeviation="0.515625"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.26562"/>
|
||||
<feGaussianBlur stdDeviation="0.632812"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.96875"/>
|
||||
<feGaussianBlur stdDeviation="1.57031"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-0.1875" dy="0.421875"/>
|
||||
<feGaussianBlur stdDeviation="0.339844"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.679688"/>
|
||||
<feGaussianBlur stdDeviation="0.269531"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
|
||||
</filter>
|
||||
<filter id="filter2_ddddi" x="4.5094" y="11.3719" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="0.328125"/>
|
||||
<feGaussianBlur stdDeviation="0.515625"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.26562"/>
|
||||
<feGaussianBlur stdDeviation="0.632812"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.96875"/>
|
||||
<feGaussianBlur stdDeviation="1.57031"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-0.1875" dy="0.421875"/>
|
||||
<feGaussianBlur stdDeviation="0.339844"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.679688"/>
|
||||
<feGaussianBlur stdDeviation="0.269531"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
|
||||
</filter>
|
||||
<filter id="filter3_ddddi" x="2.0625" y="6.47815" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="0.328125"/>
|
||||
<feGaussianBlur stdDeviation="0.515625"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.26562"/>
|
||||
<feGaussianBlur stdDeviation="0.632812"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.96875"/>
|
||||
<feGaussianBlur stdDeviation="1.57031"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-0.1875" dy="0.421875"/>
|
||||
<feGaussianBlur stdDeviation="0.339844"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.679688"/>
|
||||
<feGaussianBlur stdDeviation="0.269531"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
|
||||
</filter>
|
||||
<filter id="filter4_ddddi" x="9.40314" y="8.92499" width="12.5344" height="12.5344" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="0.328125"/>
|
||||
<feGaussianBlur stdDeviation="0.515625"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.26562"/>
|
||||
<feGaussianBlur stdDeviation="0.632812"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1.96875"/>
|
||||
<feGaussianBlur stdDeviation="1.57031"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect2_dropShadow" result="effect3_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dx="-0.1875" dy="0.421875"/>
|
||||
<feGaussianBlur stdDeviation="0.339844"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.1 0"/>
|
||||
<feBlend mode="overlay" in2="effect3_dropShadow" result="effect4_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect4_dropShadow" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="-0.679688"/>
|
||||
<feGaussianBlur stdDeviation="0.269531"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.678431 0 0 0 0 0.819608 0 0 0 0 0.726431 0 0 0 1 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect5_innerShadow"/>
|
||||
</filter>
|
||||
<linearGradient id="paint0_linear" x1="12" y1="2.34375" x2="12" y2="21.6562" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1ED9A3"/>
|
||||
<stop offset="1" stop-color="#0DBD8B"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 12 KiB |
5
res/img/element-icons/add-photo.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="84" height="84" viewBox="0 0 84 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M36.7988 34.9062C37.077 33.5217 38.2978 32.5 39.7396 32.5H44.2604C45.7022 32.5 46.923 33.5217 47.2012 34.9062C47.2429 35.1137 47.3232 35.3141 47.4627 35.4731L48.0649 36.1595C48.2548 36.3759 48.5287 36.5 48.8166 36.5H52C53.1046 36.5 54 37.3954 54 38.5V49.5C54 50.6046 53.1046 51.5 52 51.5H32C30.8954 51.5 30 50.6046 30 49.5V38.5C30 37.3954 30.8954 36.5 32 36.5H35.1834C35.4713 36.5 35.7452 36.3759 35.9351 36.1595L36.5373 35.4731C36.6768 35.3141 36.7571 35.1137 36.7988 34.9062ZM42 47.5C44.2091 47.5 46 45.7091 46 43.5C46 41.2909 44.2091 39.5 42 39.5C39.7909 39.5 38 41.2909 38 43.5C38 45.7091 39.7909 47.5 42 47.5Z" fill="white"/>
|
||||
<rect x="32" y="35" width="3" height="1" rx="0.5" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M59.75 27C59.75 26.5858 59.4142 26.25 59 26.25C58.5858 26.25 58.25 26.5858 58.25 27V31.25L54 31.25C53.5858 31.25 53.25 31.5858 53.25 32C53.25 32.4142 53.5858 32.75 54 32.75L58.25 32.75V37C58.25 37.4142 58.5858 37.75 59 37.75C59.4142 37.75 59.75 37.4142 59.75 37V32.75L64 32.75C64.4142 32.75 64.75 32.4142 64.75 32C64.75 31.5858 64.4142 31.25 64 31.25L59.75 31.25V27Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
3
res/img/element-icons/home.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.33301 7.28322V14.9493C2.33301 16.0735 3.25744 16.9776 4.38152 16.9659C4.90089 16.9605 5.44431 16.9567 6 16.9543V11.5C6 10.6716 6.67157 10 7.5 10H10.5C11.3284 10 12 10.6716 12 11.5V16.9662C12.6022 16.9703 13.1579 16.9748 13.6449 16.9791C14.7592 16.989 15.6663 16.0899 15.6663 14.9756V7.28178C15.6663 6.89062 15.4946 6.52064 15.1965 6.2673L9.97115 1.82572C9.411 1.3496 8.58834 1.3496 8.0282 1.82572L2.80281 6.2673C2.50477 6.52064 2.33301 6.89206 2.33301 7.28322Z" fill="#737D8C"/>
|
||||
</svg>
|
After Width: | Height: | Size: 634 B |
4
res/img/element-icons/info.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="10" cy="10" r="9.5" stroke="#787878"/>
|
||||
<path d="M9.79248 14H11.2065V8H9.79248V14ZM10.5034 7.14844C10.9526 7.14844 11.3198 6.80469 11.3198 6.38281C11.3198 5.95703 10.9526 5.61328 10.5034 5.61328C10.0503 5.61328 9.68311 5.95703 9.68311 6.38281C9.68311 6.80469 10.0503 7.14844 10.5034 7.14844Z" fill="#787878"/>
|
||||
</svg>
|
After Width: | Height: | Size: 424 B |
11
res/img/element-icons/room/default_app.svg
Normal file
|
@ -0,0 +1,11 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" fill="url(#paint0_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3V9.5H0.00390625L0.00390625 10.5H2V17H0.00390625L0.00390625 18H2V20H3V18H9.5039V20.0005H10.5039V18H17V20H18V18H20.0039V17H18V10.5H20.0039V9.5H18V3H20.0039V2H18V0L17 0V2H10.5039V0.000488281L9.5039 0.000488281V2H3V0L2 0V2H0.00390625L0.00390625 3H2ZM17 3H10.5039V9.5H17V3ZM17 10.5H10.5039V17H17V10.5ZM9.5039 10.5V17H3V10.5H9.5039ZM9.5039 3V9.5H3V3H9.5039Z" fill="white" fill-opacity="0.3" style="mix-blend-mode:lighten"/>
|
||||
<circle opacity="0.8" cx="10.0039" cy="10" r="7.5" stroke="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="10" y1="0" x2="10" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#60A6FF"/>
|
||||
<stop offset="1" stop-color="#418DED"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 900 B |
6
res/img/element-icons/room/default_cal.svg
Normal file
|
@ -0,0 +1,6 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.99461" y="1.00002" width="18" height="18" rx="2" fill="white" stroke="#FF4B55" stroke-width="2"/>
|
||||
<rect x="2.96777" y="2" width="16.9843" height="5" fill="#FF4B55"/>
|
||||
<rect x="4.96533" y="9" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
<rect x="11.9585" y="13.0005" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
</svg>
|
After Width: | Height: | Size: 442 B |
5
res/img/element-icons/room/default_clock.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.49609" y="0.500488" width="19" height="19" rx="3.5" fill="#17191C" stroke="#17191C"/>
|
||||
<path d="M18.9961 10.0005C18.9961 14.4188 15.4144 18.0005 10.9961 18.0005C6.57782 18.0005 2.99609 14.4188 2.99609 10.0005C2.99609 5.58221 6.57782 2.00049 10.9961 2.00049C15.4144 2.00049 18.9961 5.58221 18.9961 10.0005Z" fill="white"/>
|
||||
<path d="M10.9961 6.00049V9.81299L13.4961 11.5005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 569 B |
4
res/img/element-icons/room/default_doc.svg
Normal file
|
@ -0,0 +1,4 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="-0.000976562" y="0.000488281" width="20" height="20" rx="4" fill="#FCC639"/>
|
||||
<path d="M1.99902 7.00049H17.999V16.5005C17.999 17.3289 17.3274 18.0005 16.499 18.0005H3.49902C2.6706 18.0005 1.99902 17.3289 1.99902 16.5005V7.00049Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 364 B |
5
res/img/element-icons/room/ellipsis.svg
Normal file
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="15.5" cy="10" r="1.5" transform="rotate(180 15.5 10)" fill="#15191E"/>
|
||||
<circle cx="10" cy="10" r="1.5" transform="rotate(180 10 10)" fill="#15191E"/>
|
||||
<circle cx="4.5" cy="10" r="1.5" transform="rotate(180 4.5 10)" fill="#15191E"/>
|
||||
</svg>
|
After Width: | Height: | Size: 358 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M28 8.5C28 12.0899 25.0899 15 21.5 15C17.9101 15 15 12.0899 15 8.5C15 4.91015 17.9101 2 21.5 2C25.0899 2 28 4.91015 28 8.5ZM22.5 5C22.5 4.44772 22.0523 4 21.5 4C20.9477 4 20.5 4.44772 20.5 5V7.6286H17.9C17.4029 7.6286 17 8.02089 17 8.5048C17 8.98871 17.4029 9.381 17.9 9.381H20.5V12.0096C20.5 12.5619 20.9477 13.0096 21.5 13.0096C22.0523 13.0096 22.5 12.5619 22.5 12.0096V9.381H25.1C25.5971 9.381 26 8.98871 26 8.5048C26 8.02089 25.5971 7.6286 25.1 7.6286H22.5V5ZM21.5 16C23.6351 16 25.5619 15.1078 26.9278 13.6759C26.9755 14.1107 27 14.5525 27 15C27 18.9261 25.1146 22.4117 22.1998 24.601V24.6009C20.348 25.9918 18.0808 26.8595 15.6175 26.9844C15.413 26.9948 15.2071 27 15 27C8.37258 27 3 21.6274 3 15C3 8.37258 8.37258 3 15 3C15.4475 3 15.8893 3.0245 16.3241 3.07223C14.929 4.40304 14.0462 6.26631 14.0018 8.336C12.8183 8.89737 12 10.1031 12 11.5C12 13.433 13.567 15 15.5 15C16.0892 15 16.6445 14.8544 17.1316 14.5972C18.3618 15.4802 19.8702 16 21.5 16ZM14.9998 24.6C17.5942 24.6 19.9482 23.5709 21.6759 21.8986C20.6074 19.2607 18.0209 17.4 14.9998 17.4C11.9787 17.4 9.39221 19.2607 8.32376 21.8986C10.0514 23.5709 12.4054 24.6 14.9998 24.6Z" fill="white"/>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.1001 9C18.7779 9 18.5168 8.73883 18.5168 8.41667V6.08333H16.1834C15.8613 6.08333 15.6001 5.82217 15.6001 5.5C15.6001 5.17783 15.8613 4.91667 16.1834 4.91667H18.5168V2.58333C18.5168 2.26117 18.7779 2 19.1001 2C19.4223 2 19.6834 2.26117 19.6834 2.58333V4.91667H22.0168C22.3389 4.91667 22.6001 5.17783 22.6001 5.5C22.6001 5.82217 22.3389 6.08333 22.0168 6.08333H19.6834V8.41667C19.6834 8.73883 19.4223 9 19.1001 9ZM19.6001 11C20.0669 11 20.5212 10.9467 20.9574 10.8458C21.1161 11.5383 21.2 12.2594 21.2 13C21.2 16.1409 19.6917 18.9294 17.3598 20.6808V20.6807C16.0014 21.7011 14.3635 22.3695 12.5815 22.5505C12.2588 22.5832 11.9314 22.6 11.6 22.6C6.29807 22.6 2 18.302 2 13C2 7.69809 6.29807 3.40002 11.6 3.40002C12.3407 3.40002 13.0618 3.48391 13.7543 3.64268C13.6534 4.07884 13.6001 4.53319 13.6001 5C13.6001 8.31371 16.2864 11 19.6001 11ZM11.5999 20.68C13.6754 20.68 15.5585 19.8567 16.9407 18.5189C16.0859 16.4086 14.0167 14.92 11.5998 14.92C9.18298 14.92 7.11378 16.4086 6.25901 18.5189C7.64115 19.8567 9.52436 20.68 11.5999 20.68ZM11.7426 7.41172C10.3168 7.54168 9.2 8.74043 9.2 10.2C9.2 11.7464 10.4536 13 12 13C13.0308 13 13.9315 12.443 14.4176 11.6135C13.0673 10.6058 12.0929 9.12248 11.7426 7.41172Z" fill="black"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
7
res/img/element-icons/room/pin-upright.svg
Normal file
|
@ -0,0 +1,7 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 18.5982V15H13V18.5982C13 20.5 12.2383 22 12 22C11.7617 22 11 20.5 11 18.5982Z" fill="black"/>
|
||||
<path d="M9.5 6C9.17534 5.83333 7.78566 5.2 6.61693 4C5.4482 2.8 6.12239 2 7.13026 2H12V6H9.5Z" fill="black"/>
|
||||
<path d="M14.5 6C14.8247 5.83333 16.2143 5.2 17.3831 4C18.5518 2.8 17.8776 2 16.8697 2H12V6H14.5Z" fill="black"/>
|
||||
<path d="M9.42857 6H14.5714L15 10H9L9.42857 6Z" fill="black"/>
|
||||
<path d="M12 9C8.93114 9 6.32353 10.6927 5.37867 13.0483C4.96746 14.0735 5.89543 15 7 15H17C18.1046 15 19.0325 14.0735 18.6213 13.0483C17.6765 10.6927 15.0689 9 12 9Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 681 B |
3
res/img/element-icons/room/room-summary.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12ZM10 12C10 12.5523 10.4477 13 11 13V16.5C11 17.0523 11.4477 17.5 12 17.5H13.5C14.0523 17.5 14.5 17.0523 14.5 16.5C14.5 15.9477 14.0523 15.5 13.5 15.5H13V12C13 11.4477 12.5523 11 12 11H11C10.4477 11 10 11.4477 10 12ZM12 10C12.8284 10 13.5 9.32843 13.5 8.5C13.5 7.67157 12.8284 7 12 7C11.1716 7 10.5 7.67157 10.5 8.5C10.5 9.32843 11.1716 10 12 10Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 630 B |
|
@ -39,7 +39,7 @@ $info-plinth-fg-color: #888;
|
|||
|
||||
$preview-bar-bg-color: $header-panel-bg-color;
|
||||
|
||||
$tagpanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||
$inverted-bg-color: $base-color;
|
||||
|
||||
// used by AddressSelector
|
||||
|
@ -87,11 +87,10 @@ $dialog-background-bg-color: $header-panel-bg-color;
|
|||
$lightbox-background-bg-color: #000;
|
||||
|
||||
$settings-grey-fg-color: #a2a2a2;
|
||||
$settings-profile-placeholder-bg-color: #e7e7e7;
|
||||
$settings-profile-overlay-bg-color: #000;
|
||||
$settings-profile-overlay-placeholder-bg-color: transparent;
|
||||
$settings-profile-overlay-fg-color: #fff;
|
||||
$settings-profile-placeholder-bg-color: #21262c;
|
||||
$settings-profile-overlay-placeholder-fg-color: #454545;
|
||||
$settings-profile-button-bg-color: #e7e7e7;
|
||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
$settings-subsection-fg-color: $text-secondary-color;
|
||||
|
||||
$topleftmenu-color: $text-primary-color;
|
||||
|
@ -99,7 +98,7 @@ $roomheader-color: $text-primary-color;
|
|||
$roomheader-bg-color: $bg-color;
|
||||
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3);
|
||||
$roomheader-addroom-fg-color: $text-primary-color;
|
||||
$tagpanel-button-color: $header-panel-text-primary-color;
|
||||
$groupFilterPanel-button-color: $header-panel-text-primary-color;
|
||||
$groupheader-button-color: $header-panel-text-primary-color;
|
||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||
$icon-button-color: #8E99A4;
|
||||
|
@ -119,6 +118,8 @@ $roomlist-bg-color: rgba(33, 38, 44, 0.90);
|
|||
$roomlist-header-color: $tertiary-fg-color;
|
||||
$roomsublist-divider-color: $primary-fg-color;
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: rgba(141, 151, 165, 0.2);
|
||||
|
@ -186,7 +187,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
|||
|
||||
$kbd-border-color: #000000;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
|
@ -201,7 +202,7 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
|
||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||
$roomlist-background-blur-amount: 60px;
|
||||
$tagpanel-background-blur-amount: 30px;
|
||||
$groupFilterPanel-background-blur-amount: 30px;
|
||||
|
||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
@import "../../light/css/_fonts.scss";
|
||||
@import "../../light/css/_light.scss";
|
||||
// important this goes before _mods,
|
||||
// as $tagpanel-background-blur-amount and
|
||||
// as $groupFilterPanel-background-blur-amount and
|
||||
// $roomlist-background-blur-amount
|
||||
// are overridden in _dark.scss
|
||||
@import "_dark.scss";
|
||||
|
|
|
@ -37,8 +37,8 @@ $info-plinth-fg-color: #888;
|
|||
|
||||
$preview-bar-bg-color: $header-panel-bg-color;
|
||||
|
||||
$tagpanel-bg-color: $base-color;
|
||||
$inverted-bg-color: $tagpanel-bg-color;
|
||||
$groupFilterPanel-bg-color: $base-color;
|
||||
$inverted-bg-color: $groupFilterPanel-bg-color;
|
||||
|
||||
// used by AddressSelector
|
||||
$selected-color: $room-highlight-color;
|
||||
|
@ -86,17 +86,16 @@ $lightbox-background-bg-color: #000;
|
|||
|
||||
$settings-grey-fg-color: #a2a2a2;
|
||||
$settings-profile-placeholder-bg-color: #e7e7e7;
|
||||
$settings-profile-overlay-bg-color: #000;
|
||||
$settings-profile-overlay-placeholder-bg-color: transparent;
|
||||
$settings-profile-overlay-fg-color: #fff;
|
||||
$settings-profile-overlay-placeholder-fg-color: #454545;
|
||||
$settings-profile-button-bg-color: #e7e7e7;
|
||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
$settings-subsection-fg-color: $text-secondary-color;
|
||||
|
||||
$topleftmenu-color: $text-primary-color;
|
||||
$roomheader-color: $text-primary-color;
|
||||
$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity
|
||||
$roomheader-addroom-fg-color: $text-primary-color;
|
||||
$tagpanel-button-color: $header-panel-text-primary-color;
|
||||
$groupFilterPanel-button-color: $header-panel-text-primary-color;
|
||||
$groupheader-button-color: $header-panel-text-primary-color;
|
||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||
$icon-button-color: $header-panel-text-primary-color;
|
||||
|
@ -116,6 +115,8 @@ $roomlist-bg-color: $header-panel-bg-color;
|
|||
|
||||
$roomsublist-divider-color: $primary-fg-color;
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #1A1D23;
|
||||
|
@ -181,7 +182,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
|||
|
||||
$kbd-border-color: #000000;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
|
|
|
@ -67,8 +67,8 @@ $preview-bar-bg-color: #f7f7f7;
|
|||
$secondary-accent-color: #f2f5f8;
|
||||
$tertiary-accent-color: #d3efe1;
|
||||
|
||||
$tagpanel-bg-color: #27303a;
|
||||
$inverted-bg-color: $tagpanel-bg-color;
|
||||
$groupFilterPanel-bg-color: #27303a;
|
||||
$inverted-bg-color: $groupFilterPanel-bg-color;
|
||||
|
||||
// used by RoomDirectory permissions
|
||||
$plinth-bg-color: $secondary-accent-color;
|
||||
|
@ -144,10 +144,9 @@ $blockquote-fg-color: #777;
|
|||
|
||||
$settings-grey-fg-color: #a2a2a2;
|
||||
$settings-profile-placeholder-bg-color: #e7e7e7;
|
||||
$settings-profile-overlay-bg-color: #000;
|
||||
$settings-profile-overlay-placeholder-bg-color: transparent;
|
||||
$settings-profile-overlay-fg-color: #fff;
|
||||
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
|
||||
$settings-profile-button-bg-color: #e7e7e7;
|
||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
$settings-subsection-fg-color: #61708b;
|
||||
|
||||
$voip-decline-color: #f48080;
|
||||
|
@ -163,7 +162,7 @@ $roomheader-color: #45474a;
|
|||
$roomheader-bg-color: $primary-bg-color;
|
||||
$roomheader-addroom-bg-color: #91a1c0;
|
||||
$roomheader-addroom-fg-color: $accent-fg-color;
|
||||
$tagpanel-button-color: #91a1c0;
|
||||
$groupFilterPanel-button-color: #91a1c0;
|
||||
$groupheader-button-color: #91a1c0;
|
||||
$rightpanel-button-color: #91a1c0;
|
||||
$icon-button-color: #91a1c0;
|
||||
|
@ -183,6 +182,8 @@ $roomlist-bg-color: $header-panel-bg-color;
|
|||
$roomlist-header-color: $primary-fg-color;
|
||||
$roomsublist-divider-color: $primary-fg-color;
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #fff;
|
||||
|
@ -304,7 +305,7 @@ $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-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
|
|
|
@ -49,7 +49,7 @@ $roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
|||
//
|
||||
// --sidebar-color
|
||||
$interactive-tooltip-bg-color: var(--sidebar-color);
|
||||
$tagpanel-bg-color: var(--sidebar-color);
|
||||
$groupFilterPanel-bg-color: var(--sidebar-color);
|
||||
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||
$roomlist-button-bg-color: var(--sidebar-color-15pct);
|
||||
|
@ -124,15 +124,15 @@ $pinned-unread-color: var(--warning-color);
|
|||
$warning-color: var(--warning-color);
|
||||
$button-danger-disabled-bg-color: var(--warning-color-50pct); // still needs alpha at 0.5
|
||||
//
|
||||
// --username colors
|
||||
$username-variant1-color: var(--username-colors_1, $username-variant1-color);
|
||||
$username-variant2-color: var(--username-colors_2, $username-variant2-color);
|
||||
$username-variant3-color: var(--username-colors_3, $username-variant3-color);
|
||||
$username-variant4-color: var(--username-colors_4, $username-variant4-color);
|
||||
$username-variant5-color: var(--username-colors_5, $username-variant5-color);
|
||||
$username-variant6-color: var(--username-colors_6, $username-variant6-color);
|
||||
$username-variant7-color: var(--username-colors_7, $username-variant7-color);
|
||||
$username-variant8-color: var(--username-colors_8, $username-variant8-color);
|
||||
// --username colors (which use a 0-based index)
|
||||
$username-variant1-color: var(--username-colors_0, $username-variant1-color);
|
||||
$username-variant2-color: var(--username-colors_1, $username-variant2-color);
|
||||
$username-variant3-color: var(--username-colors_2, $username-variant3-color);
|
||||
$username-variant4-color: var(--username-colors_3, $username-variant4-color);
|
||||
$username-variant5-color: var(--username-colors_4, $username-variant5-color);
|
||||
$username-variant6-color: var(--username-colors_5, $username-variant6-color);
|
||||
$username-variant7-color: var(--username-colors_6, $username-variant7-color);
|
||||
$username-variant8-color: var(--username-colors_7, $username-variant8-color);
|
||||
//
|
||||
// --timeline-highlights-color
|
||||
$event-selected-color: var(--timeline-highlights-color);
|
||||
|
|
|
@ -62,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7;
|
|||
$secondary-accent-color: #f2f5f8;
|
||||
$tertiary-accent-color: #d3efe1;
|
||||
|
||||
$tagpanel-bg-color: rgba(232, 232, 232, 0.77);
|
||||
$groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
|
||||
|
||||
// used by RoomDirectory permissions
|
||||
$plinth-bg-color: $secondary-accent-color;
|
||||
|
@ -137,11 +137,10 @@ $blockquote-bar-color: #ddd;
|
|||
$blockquote-fg-color: #777;
|
||||
|
||||
$settings-grey-fg-color: #a2a2a2;
|
||||
$settings-profile-placeholder-bg-color: #e7e7e7;
|
||||
$settings-profile-overlay-bg-color: #000;
|
||||
$settings-profile-overlay-placeholder-bg-color: transparent;
|
||||
$settings-profile-overlay-fg-color: #fff;
|
||||
$settings-profile-placeholder-bg-color: #f4f6fa;
|
||||
$settings-profile-overlay-placeholder-fg-color: #2e2f32;
|
||||
$settings-profile-button-bg-color: #e7e7e7;
|
||||
$settings-profile-button-fg-color: $settings-profile-overlay-placeholder-fg-color;
|
||||
$settings-subsection-fg-color: #61708b;
|
||||
|
||||
$voip-decline-color: #f48080;
|
||||
|
@ -157,7 +156,7 @@ $roomheader-color: #45474a;
|
|||
$roomheader-bg-color: $primary-bg-color;
|
||||
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2);
|
||||
$roomheader-addroom-fg-color: #5c6470;
|
||||
$tagpanel-button-color: #91A1C0;
|
||||
$groupFilterPanel-button-color: #91A1C0;
|
||||
$groupheader-button-color: #91A1C0;
|
||||
$rightpanel-button-color: #91A1C0;
|
||||
$icon-button-color: #C1C6CD;
|
||||
|
@ -177,6 +176,8 @@ $roomlist-bg-color: rgba(245, 245, 245, 0.90);
|
|||
$roomlist-header-color: $tertiary-fg-color;
|
||||
$roomsublist-divider-color: $primary-fg-color;
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
$roomtile-selected-bg-color: #FFF;
|
||||
|
@ -319,7 +320,7 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
|
||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||
$roomlist-background-blur-amount: 40px;
|
||||
$tagpanel-background-blur-amount: 20px;
|
||||
$groupFilterPanel-background-blur-amount: 20px;
|
||||
|
||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
@supports (backdrop-filter: none) {
|
||||
.mx_LeftPanel {
|
||||
background-image: var(--avatar-url);
|
||||
background-image: var(--avatar-url, unset);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: left top;
|
||||
}
|
||||
|
||||
.mx_TagPanel {
|
||||
backdrop-filter: blur($tagpanel-background-blur-amount);
|
||||
.mx_GroupFilterPanel {
|
||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||
|
|
16
src/@types/global.d.ts
vendored
|
@ -14,6 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
|
||||
import * as ModernizrStatic from "modernizr";
|
||||
import ContentMessages from "../ContentMessages";
|
||||
import { IMatrixClientPeg } from "../MatrixClientPeg";
|
||||
|
@ -27,10 +28,17 @@ import {ModalManager} from "../Modal";
|
|||
import SettingsStore from "../settings/SettingsStore";
|
||||
import {ActiveRoomObserver} from "../ActiveRoomObserver";
|
||||
import {Notifier} from "../Notifier";
|
||||
import type {Renderer} from "react-dom";
|
||||
import RightPanelStore from "../stores/RightPanelStore";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
import CallHandler from "../CallHandler";
|
||||
import {Analytics} from "../Analytics";
|
||||
import UserActivity from "../UserActivity";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Modernizr: ModernizrStatic;
|
||||
matrixChat: ReturnType<Renderer>;
|
||||
mxMatrixClientPeg: IMatrixClientPeg;
|
||||
Olm: {
|
||||
init: () => Promise<void>;
|
||||
|
@ -47,6 +55,11 @@ declare global {
|
|||
singletonModalManager: ModalManager;
|
||||
mxSettingsStore: SettingsStore;
|
||||
mxNotifier: typeof Notifier;
|
||||
mxRightPanelStore: RightPanelStore;
|
||||
mxWidgetStore: WidgetStore;
|
||||
mxCallHandler: CallHandler;
|
||||
mxAnalytics: Analytics;
|
||||
mxUserActivity: UserActivity;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
|
@ -56,6 +69,9 @@ declare global {
|
|||
|
||||
interface Navigator {
|
||||
userLanguage?: string;
|
||||
// https://github.com/Microsoft/TypeScript/issues/19473
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MediaSession
|
||||
mediaSession: any;
|
||||
}
|
||||
|
||||
interface StorageEstimate {
|
||||
|
|
23
src/@types/sanitize-html.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
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 sanitizeHtml from 'sanitize-html';
|
||||
|
||||
export interface IExtendedSanitizeOptions extends sanitizeHtml.IOptions {
|
||||
// This option only exists in 2.x RCs so far, so not yet present in the
|
||||
// separate type definition module.
|
||||
nestingLimit?: number;
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||
import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import Modal from './Modal';
|
||||
|
@ -27,7 +27,7 @@ const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password
|
|||
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
||||
|
||||
// Remove all but the first item in the hash path. Redact unexpected hashes.
|
||||
function getRedactedHash(hash) {
|
||||
function getRedactedHash(hash: string): string {
|
||||
// Don't leak URLs we aren't expecting - they could contain tokens/PII
|
||||
const match = hashRegex.exec(hash);
|
||||
if (!match) {
|
||||
|
@ -44,7 +44,7 @@ function getRedactedHash(hash) {
|
|||
|
||||
// Return the current origin, path and hash separated with a `/`. This does
|
||||
// not include query parameters.
|
||||
function getRedactedUrl() {
|
||||
function getRedactedUrl(): string {
|
||||
const { origin, hash } = window.location;
|
||||
let { pathname } = window.location;
|
||||
|
||||
|
@ -56,7 +56,25 @@ function getRedactedUrl() {
|
|||
return origin + pathname + getRedactedHash(hash);
|
||||
}
|
||||
|
||||
const customVariables = {
|
||||
interface IData {
|
||||
/* eslint-disable camelcase */
|
||||
gt_ms?: string;
|
||||
e_c?: string;
|
||||
e_a?: string;
|
||||
e_n?: string;
|
||||
e_v?: string;
|
||||
ping?: string;
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
||||
interface IVariable {
|
||||
id: number;
|
||||
expl: string; // explanation
|
||||
example: string; // example value
|
||||
getTextVariables?(): IVariables; // object to pass as 2nd argument to `_t`
|
||||
}
|
||||
|
||||
const customVariables: Record<string, IVariable> = {
|
||||
// The Matomo installation at https://matomo.riot.im is currently configured
|
||||
// with a limit of 10 custom variables.
|
||||
'App Platform': {
|
||||
|
@ -120,7 +138,7 @@ const customVariables = {
|
|||
},
|
||||
};
|
||||
|
||||
function whitelistRedact(whitelist, str) {
|
||||
function whitelistRedact(whitelist: string[], str: string): string {
|
||||
if (whitelist.includes(str)) return str;
|
||||
return '<redacted>';
|
||||
}
|
||||
|
@ -130,7 +148,7 @@ const CREATION_TS_KEY = "mx_Riot_Analytics_cts";
|
|||
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
|
||||
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
|
||||
|
||||
function getUid() {
|
||||
function getUid(): string {
|
||||
try {
|
||||
let data = localStorage && localStorage.getItem(UID_KEY);
|
||||
if (!data && localStorage) {
|
||||
|
@ -145,97 +163,105 @@ function getUid() {
|
|||
|
||||
const HEARTBEAT_INTERVAL = 30 * 1000; // seconds
|
||||
|
||||
class Analytics {
|
||||
export class Analytics {
|
||||
private baseUrl: URL = null;
|
||||
private siteId: string = null;
|
||||
private visitVariables: Record<number, [string, string]> = {}; // {[id: number]: [name: string, value: string]}
|
||||
private firstPage = true;
|
||||
private heartbeatIntervalID: number = null;
|
||||
|
||||
private readonly creationTs: string;
|
||||
private readonly lastVisitTs: string;
|
||||
private readonly visitCount: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = null;
|
||||
this.siteId = null;
|
||||
this.visitVariables = {};
|
||||
|
||||
this.firstPage = true;
|
||||
this._heartbeatIntervalID = null;
|
||||
|
||||
this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY);
|
||||
if (!this.creationTs && localStorage) {
|
||||
localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime());
|
||||
localStorage.setItem(CREATION_TS_KEY, this.creationTs = String(new Date().getTime()));
|
||||
}
|
||||
|
||||
this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY);
|
||||
this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0;
|
||||
this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || "0";
|
||||
this.visitCount = String(parseInt(this.visitCount, 10) + 1); // increment
|
||||
if (localStorage) {
|
||||
localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1);
|
||||
localStorage.setItem(VISIT_COUNT_KEY, this.visitCount);
|
||||
}
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
public get disabled() {
|
||||
return !this.baseUrl;
|
||||
}
|
||||
|
||||
public canEnable() {
|
||||
const config = SdkConfig.get();
|
||||
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable Analytics if initialized but disabled
|
||||
* otherwise try and initalize, no-op if piwik config missing
|
||||
*/
|
||||
async enable() {
|
||||
public async enable() {
|
||||
if (!this.disabled) return;
|
||||
|
||||
if (!this.canEnable()) return;
|
||||
const config = SdkConfig.get();
|
||||
if (!config || !config.piwik || !config.piwik.url || !config.piwik.siteId) return;
|
||||
|
||||
this.baseUrl = new URL("piwik.php", config.piwik.url);
|
||||
// set constants
|
||||
this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking
|
||||
this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking
|
||||
this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking
|
||||
this.baseUrl.searchParams.set("apiv", 1); // API version to use
|
||||
this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF
|
||||
this.baseUrl.searchParams.set("apiv", "1"); // API version to use
|
||||
this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF
|
||||
// set user parameters
|
||||
this.baseUrl.searchParams.set("_id", getUid()); // uuid
|
||||
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts
|
||||
this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count
|
||||
this.baseUrl.searchParams.set("_idvc", this.visitCount); // visit count
|
||||
if (this.lastVisitTs) {
|
||||
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
|
||||
}
|
||||
|
||||
const platform = PlatformPeg.get();
|
||||
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||
this.setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||
try {
|
||||
this._setVisitVariable('App Version', await platform.getAppVersion());
|
||||
this.setVisitVariable('App Version', await platform.getAppVersion());
|
||||
} catch (e) {
|
||||
this._setVisitVariable('App Version', 'unknown');
|
||||
this.setVisitVariable('App Version', 'unknown');
|
||||
}
|
||||
|
||||
this._setVisitVariable('Chosen Language', getCurrentLanguage());
|
||||
this.setVisitVariable('Chosen Language', getCurrentLanguage());
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname === 'riot.im') {
|
||||
this._setVisitVariable('Instance', window.location.pathname);
|
||||
this.setVisitVariable('Instance', window.location.pathname);
|
||||
} else if (hostname.endsWith('.element.io')) {
|
||||
this._setVisitVariable('Instance', hostname.replace('.element.io', ''));
|
||||
this.setVisitVariable('Instance', hostname.replace('.element.io', ''));
|
||||
}
|
||||
|
||||
let installedPWA = "unknown";
|
||||
try {
|
||||
// Known to work at least for desktop Chrome
|
||||
installedPWA = window.matchMedia('(display-mode: standalone)').matches;
|
||||
installedPWA = String(window.matchMedia('(display-mode: standalone)').matches);
|
||||
} catch (e) { }
|
||||
this._setVisitVariable('Installed PWA', installedPWA);
|
||||
this.setVisitVariable('Installed PWA', installedPWA);
|
||||
|
||||
let touchInput = "unknown";
|
||||
try {
|
||||
// MDN claims broad support across browsers
|
||||
touchInput = window.matchMedia('(pointer: coarse)').matches;
|
||||
touchInput = String(window.matchMedia('(pointer: coarse)').matches);
|
||||
} catch (e) { }
|
||||
this._setVisitVariable('Touch Input', touchInput);
|
||||
this.setVisitVariable('Touch Input', touchInput);
|
||||
|
||||
// start heartbeat
|
||||
this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
|
||||
this.heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Analytics, stop the heartbeat and clear identifiers from localStorage
|
||||
*/
|
||||
disable() {
|
||||
public disable() {
|
||||
if (this.disabled) return;
|
||||
this.trackEvent('Analytics', 'opt-out');
|
||||
window.clearInterval(this._heartbeatIntervalID);
|
||||
window.clearInterval(this.heartbeatIntervalID);
|
||||
this.baseUrl = null;
|
||||
this.visitVariables = {};
|
||||
localStorage.removeItem(UID_KEY);
|
||||
|
@ -244,7 +270,7 @@ class Analytics {
|
|||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||
}
|
||||
|
||||
async _track(data) {
|
||||
private async _track(data: IData) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const now = new Date();
|
||||
|
@ -260,13 +286,13 @@ class Analytics {
|
|||
s: now.getSeconds(),
|
||||
};
|
||||
|
||||
const url = new URL(this.baseUrl);
|
||||
const url = new URL(this.baseUrl.toString()); // copy
|
||||
for (const key in params) {
|
||||
url.searchParams.set(key, params[key]);
|
||||
}
|
||||
|
||||
try {
|
||||
await window.fetch(url, {
|
||||
await window.fetch(url.toString(), {
|
||||
method: "GET",
|
||||
mode: "no-cors",
|
||||
cache: "no-cache",
|
||||
|
@ -277,14 +303,14 @@ class Analytics {
|
|||
}
|
||||
}
|
||||
|
||||
ping() {
|
||||
public ping() {
|
||||
this._track({
|
||||
ping: 1,
|
||||
ping: "1",
|
||||
});
|
||||
localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts
|
||||
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
|
||||
}
|
||||
|
||||
trackPageChange(generationTimeMs) {
|
||||
public trackPageChange(generationTimeMs?: number) {
|
||||
if (this.disabled) return;
|
||||
if (this.firstPage) {
|
||||
// De-duplicate first page
|
||||
|
@ -299,11 +325,11 @@ class Analytics {
|
|||
}
|
||||
|
||||
this._track({
|
||||
gt_ms: generationTimeMs,
|
||||
gt_ms: String(generationTimeMs),
|
||||
});
|
||||
}
|
||||
|
||||
trackEvent(category, action, name, value) {
|
||||
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
||||
if (this.disabled) return;
|
||||
this._track({
|
||||
e_c: category,
|
||||
|
@ -313,12 +339,12 @@ class Analytics {
|
|||
});
|
||||
}
|
||||
|
||||
_setVisitVariable(key, value) {
|
||||
private setVisitVariable(key: keyof typeof customVariables, value: string) {
|
||||
if (this.disabled) return;
|
||||
this.visitVariables[customVariables[key].id] = [key, value];
|
||||
}
|
||||
|
||||
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||
public setLoggedIn(isGuest: boolean, homeserverUrl: string) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const config = SdkConfig.get();
|
||||
|
@ -326,16 +352,16 @@ class Analytics {
|
|||
|
||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
||||
|
||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||
this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||
this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||
}
|
||||
|
||||
setBreadcrumbs(state) {
|
||||
public setBreadcrumbs(state: boolean) {
|
||||
if (this.disabled) return;
|
||||
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||
this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||
}
|
||||
|
||||
showDetailsModal = () => {
|
||||
public showDetailsModal = () => {
|
||||
let rows = [];
|
||||
if (!this.disabled) {
|
||||
rows = Object.values(this.visitVariables);
|
||||
|
@ -356,7 +382,7 @@ class Analytics {
|
|||
'e.g. <CurrentPageURL>',
|
||||
{},
|
||||
{
|
||||
CurrentPageURL: getRedactedUrl(),
|
||||
CurrentPageURL: getRedactedUrl,
|
||||
},
|
||||
),
|
||||
},
|
||||
|
@ -397,7 +423,7 @@ class Analytics {
|
|||
};
|
||||
}
|
||||
|
||||
if (!global.mxAnalytics) {
|
||||
global.mxAnalytics = new Analytics();
|
||||
if (!window.mxAnalytics) {
|
||||
window.mxAnalytics = new Analytics();
|
||||
}
|
||||
export default global.mxAnalytics;
|
||||
export default window.mxAnalytics;
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import createReactClass from 'create-react-class';
|
||||
import React from "react";
|
||||
import * as sdk from './index';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from './languageHandler';
|
||||
|
@ -24,21 +24,19 @@ import { _t } from './languageHandler';
|
|||
* Wrap an asynchronous loader function with a react component which shows a
|
||||
* spinner until the real component loads.
|
||||
*/
|
||||
export default createReactClass({
|
||||
propTypes: {
|
||||
export default class AsyncWrapper extends React.Component {
|
||||
static propTypes = {
|
||||
/** A promise which resolves with the real component
|
||||
*/
|
||||
prom: PropTypes.object.isRequired,
|
||||
},
|
||||
};
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
state = {
|
||||
component: null,
|
||||
error: null,
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
componentDidMount() {
|
||||
this._unmounted = false;
|
||||
// XXX: temporary logging to try to diagnose
|
||||
// https://github.com/vector-im/element-web/issues/3148
|
||||
|
@ -56,17 +54,17 @@ export default createReactClass({
|
|||
console.warn('AsyncWrapper promise failed', e);
|
||||
this.setState({error: e});
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
componentWillUnmount: function() {
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
},
|
||||
}
|
||||
|
||||
_onWrapperCancelClick: function() {
|
||||
_onWrapperCancelClick = () => {
|
||||
this.props.onFinished(false);
|
||||
},
|
||||
};
|
||||
|
||||
render: function() {
|
||||
render() {
|
||||
if (this.state.component) {
|
||||
const Component = this.state.component;
|
||||
return <Component {...this.props} />;
|
||||
|
@ -87,6 +85,6 @@ export default createReactClass({
|
|||
const Spinner = sdk.getComponent("elements.Spinner");
|
||||
return <Spinner />;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import {User} from "matrix-js-sdk/src/models/user";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(member, width, height, resizeMethod) {
|
||||
let url;
|
||||
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||
let url: string;
|
||||
if (member && member.getAvatarUrl) {
|
||||
url = member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
|
@ -41,7 +46,7 @@ export function avatarUrlForMember(member, width, height, resizeMethod) {
|
|||
return url;
|
||||
}
|
||||
|
||||
export function avatarUrlForUser(user, width, height, resizeMethod) {
|
||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
const url = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
|
@ -54,14 +59,14 @@ export function avatarUrlForUser(user, width, height, resizeMethod) {
|
|||
return url;
|
||||
}
|
||||
|
||||
function isValidHexColor(color) {
|
||||
function isValidHexColor(color: string): boolean {
|
||||
return typeof color === "string" &&
|
||||
(color.length === 7 || color.lengh === 9) &&
|
||||
(color.length === 7 || color.length === 9) &&
|
||||
color.charAt(0) === "#" &&
|
||||
!color.substr(1).split("").some(c => isNaN(parseInt(c, 16)));
|
||||
}
|
||||
|
||||
function urlForColor(color) {
|
||||
function urlForColor(color: string): string {
|
||||
const size = 40;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
|
@ -79,9 +84,10 @@ function urlForColor(color) {
|
|||
// XXX: Ideally we'd clear this cache when the theme changes
|
||||
// but since this function is at global scope, it's a bit
|
||||
// hard to install a listener here, even if there were a clear event to listen to
|
||||
const colorToDataURLCache = new Map();
|
||||
const colorToDataURLCache = new Map<string, string>();
|
||||
|
||||
export function defaultAvatarUrlForString(s) {
|
||||
export function defaultAvatarUrlForString(s: string): string {
|
||||
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
|
||||
const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
|
||||
let total = 0;
|
||||
for (let i = 0; i < s.length; ++i) {
|
||||
|
@ -112,7 +118,7 @@ export function defaultAvatarUrlForString(s) {
|
|||
* @param {string} name
|
||||
* @return {string} the first letter
|
||||
*/
|
||||
export function getInitialLetter(name) {
|
||||
export function getInitialLetter(name: string): string {
|
||||
if (!name) {
|
||||
// XXX: We should find out what causes the name to sometimes be falsy.
|
||||
console.trace("`name` argument to `getInitialLetter` not supplied");
|
||||
|
@ -145,7 +151,7 @@ export function getInitialLetter(name) {
|
|||
return firstChar.toUpperCase();
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
|
@ -1,509 +0,0 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Manages a list of all the currently active calls.
|
||||
*
|
||||
* This handler dispatches when voip calls are added/updated/removed from this list:
|
||||
* {
|
||||
* action: 'call_state'
|
||||
* room_id: <room ID of the call>
|
||||
* }
|
||||
*
|
||||
* To know the state of the call, this handler exposes a getter to
|
||||
* obtain the call for a room:
|
||||
* var call = CallHandler.getCall(roomId)
|
||||
* var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
||||
*
|
||||
* This handler listens for and handles the following actions:
|
||||
* {
|
||||
* action: 'place_call',
|
||||
* type: 'voice|video',
|
||||
* room_id: <room that the place call button was pressed in>
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* action: 'incoming_call'
|
||||
* call: MatrixCall
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* action: 'hangup'
|
||||
* room_id: <room that the hangup button was pressed in>
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* action: 'answer'
|
||||
* room_id: <room that the answer button was pressed in>
|
||||
* }
|
||||
*/
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import { _t } from './languageHandler';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import {generateHumanReadableId} from "./utils/NamingUtils";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {WidgetType} from "./widgets/WidgetType";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
|
||||
global.mxCalls = {
|
||||
//room_id: MatrixCall
|
||||
};
|
||||
const calls = global.mxCalls;
|
||||
let ConferenceHandler = null;
|
||||
|
||||
const audioPromises = {};
|
||||
|
||||
function play(audioId) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
const audio = document.getElementById(audioId);
|
||||
if (audio) {
|
||||
const playAudio = async () => {
|
||||
try {
|
||||
// This still causes the chrome debugger to break on promise rejection if
|
||||
// the promise is rejected, even though we're catching the exception.
|
||||
await audio.play();
|
||||
} catch (e) {
|
||||
// This is usually because the user hasn't interacted with the document,
|
||||
// or chrome doesn't think so and is denying the request. Not sure what
|
||||
// we can really do here...
|
||||
// https://github.com/vector-im/element-web/issues/7657
|
||||
console.log("Unable to play audio clip", e);
|
||||
}
|
||||
};
|
||||
if (audioPromises[audioId]) {
|
||||
audioPromises[audioId] = audioPromises[audioId].then(()=>{
|
||||
audio.load();
|
||||
return playAudio();
|
||||
});
|
||||
} else {
|
||||
audioPromises[audioId] = playAudio();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function pause(audioId) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
const audio = document.getElementById(audioId);
|
||||
if (audio) {
|
||||
if (audioPromises[audioId]) {
|
||||
audioPromises[audioId] = audioPromises[audioId].then(()=>audio.pause());
|
||||
} else {
|
||||
// pause doesn't actually return a promise, but might as well do this for symmetry with play();
|
||||
audioPromises[audioId] = audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _setCallListeners(call) {
|
||||
call.on("error", function(err) {
|
||||
console.error("Call error:", err);
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||
) {
|
||||
_showICEFallbackPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||
title: _t('Call Failed'),
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
call.on("hangup", function() {
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
});
|
||||
// map web rtc states to dummy UI state
|
||||
// ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
||||
call.on("state", function(newState, oldState) {
|
||||
if (newState === "ringing") {
|
||||
_setCallState(call, call.roomId, "ringing");
|
||||
pause("ringbackAudio");
|
||||
} else if (newState === "invite_sent") {
|
||||
_setCallState(call, call.roomId, "ringback");
|
||||
play("ringbackAudio");
|
||||
} else if (newState === "ended" && oldState === "connected") {
|
||||
_setCallState(undefined, call.roomId, "ended");
|
||||
pause("ringbackAudio");
|
||||
play("callendAudio");
|
||||
} else if (newState === "ended" && oldState === "invite_sent" &&
|
||||
(call.hangupParty === "remote" ||
|
||||
(call.hangupParty === "local" && call.hangupReason === "invite_timeout")
|
||||
)) {
|
||||
_setCallState(call, call.roomId, "busy");
|
||||
pause("ringbackAudio");
|
||||
play("busyAudio");
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||
title: _t('Call Timeout'),
|
||||
description: _t('The remote side failed to pick up') + '.',
|
||||
});
|
||||
} else if (oldState === "invite_sent") {
|
||||
_setCallState(call, call.roomId, "stop_ringback");
|
||||
pause("ringbackAudio");
|
||||
} else if (oldState === "ringing") {
|
||||
_setCallState(call, call.roomId, "stop_ringing");
|
||||
pause("ringbackAudio");
|
||||
} else if (newState === "connected") {
|
||||
_setCallState(call, call.roomId, "connected");
|
||||
pause("ringbackAudio");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _setCallState(call, roomId, status) {
|
||||
console.log(
|
||||
`Call state in ${roomId} changed to ${status} (${call ? call.call_state : "-"})`,
|
||||
);
|
||||
calls[roomId] = call;
|
||||
|
||||
if (status === "ringing") {
|
||||
play("ringAudio");
|
||||
} else if (call && call.call_state === "ringing") {
|
||||
pause("ringAudio");
|
||||
}
|
||||
|
||||
if (call) {
|
||||
call.call_state = status;
|
||||
}
|
||||
dis.dispatch({
|
||||
action: 'call_state',
|
||||
room_id: roomId,
|
||||
state: status,
|
||||
});
|
||||
}
|
||||
|
||||
function _showICEFallbackPrompt() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const code = sub => <code>{sub}</code>;
|
||||
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
||||
title: _t("Call failed due to misconfigured server"),
|
||||
description: <div>
|
||||
<p>{_t(
|
||||
"Please ask the administrator of your homeserver " +
|
||||
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
||||
"order for calls to work reliably.",
|
||||
{ homeserverDomain: cli.getDomain() }, { code },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Alternatively, you can try to use the public server at " +
|
||||
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
|
||||
"it will share your IP address with that server. You can also manage " +
|
||||
"this in Settings.",
|
||||
null, { code },
|
||||
)}</p>
|
||||
</div>,
|
||||
button: _t('Try using turn.matrix.org'),
|
||||
cancelButton: _t('OK'),
|
||||
onFinished: (allow) => {
|
||||
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
|
||||
cli.setFallbackICEServerAllowed(allow);
|
||||
},
|
||||
}, null, true);
|
||||
}
|
||||
|
||||
function _onAction(payload) {
|
||||
function placeCall(newCall) {
|
||||
_setCallListeners(newCall);
|
||||
if (payload.type === 'voice') {
|
||||
newCall.placeVoiceCall();
|
||||
} else if (payload.type === 'video') {
|
||||
newCall.placeVideoCall(
|
||||
payload.remote_element,
|
||||
payload.local_element,
|
||||
);
|
||||
} else if (payload.type === 'screensharing') {
|
||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||
if (screenCapErrorString) {
|
||||
_setCallState(undefined, newCall.roomId, "ended");
|
||||
console.log("Can't capture screen: " + screenCapErrorString);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
||||
title: _t('Unable to capture screen'),
|
||||
description: screenCapErrorString,
|
||||
});
|
||||
return;
|
||||
}
|
||||
newCall.placeScreenSharingCall(
|
||||
payload.remote_element,
|
||||
payload.local_element,
|
||||
);
|
||||
} else {
|
||||
console.error("Unknown conf call type: %s", payload.type);
|
||||
}
|
||||
}
|
||||
|
||||
switch (payload.action) {
|
||||
case 'place_call':
|
||||
{
|
||||
if (callHandler.getAnyActiveCall()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Existing Call'),
|
||||
description: _t('You are already in a call.'),
|
||||
});
|
||||
return; // don't allow >1 call to be placed.
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (!room) {
|
||||
console.error("Room %s does not exist.", payload.room_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const members = room.getJoinedMembers();
|
||||
if (members.length <= 1) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||
description: _t('You cannot place a call with yourself.'),
|
||||
});
|
||||
return;
|
||||
} else if (members.length === 2) {
|
||||
console.info("Place %s call in %s", payload.type, payload.room_id);
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), payload.room_id);
|
||||
placeCall(call);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
room_id: payload.room_id,
|
||||
type: payload.type,
|
||||
remote_element: payload.remote_element,
|
||||
local_element: payload.local_element,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'place_conference_call':
|
||||
console.info("Place conference call in %s", payload.room_id);
|
||||
_startCallApp(payload.room_id, payload.type);
|
||||
break;
|
||||
case 'incoming_call':
|
||||
{
|
||||
if (callHandler.getAnyActiveCall()) {
|
||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||
// in future we could signal a "local busy" as a warning to the caller.
|
||||
// see https://github.com/vector-im/vector-web/issues/1964
|
||||
return;
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, stop here.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const call = payload.call;
|
||||
_setCallListeners(call);
|
||||
_setCallState(call, call.roomId, "ringing");
|
||||
}
|
||||
break;
|
||||
case 'hangup':
|
||||
if (!calls[payload.room_id]) {
|
||||
return; // no call to hangup
|
||||
}
|
||||
calls[payload.room_id].hangup();
|
||||
_setCallState(null, payload.room_id, "ended");
|
||||
break;
|
||||
case 'answer':
|
||||
if (!calls[payload.room_id]) {
|
||||
return; // no call to answer
|
||||
}
|
||||
calls[payload.room_id].answer();
|
||||
_setCallState(calls[payload.room_id], payload.room_id, "connected");
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: payload.room_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function _startCallApp(roomId, type) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
||||
|
||||
if (WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI)) {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
||||
title: _t('Call in Progress'),
|
||||
description: _t('A call is currently being placed!'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentJitsiWidgets.length > 0) {
|
||||
console.warn(
|
||||
"Refusing to start conference call widget in " + roomId +
|
||||
" a conference call widget is already present",
|
||||
);
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Already have Jitsi Widget', '', ErrorDialog, {
|
||||
title: _t('Call in Progress'),
|
||||
description: _t('A call is already in progress!'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const confId = `JitsiConference${generateHumanReadableId()}`;
|
||||
const jitsiDomain = Jitsi.getInstance().preferredDomain;
|
||||
|
||||
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl();
|
||||
|
||||
// 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_' +
|
||||
MatrixClientPeg.get().credentials.userId +
|
||||
'_' +
|
||||
Date.now()
|
||||
);
|
||||
|
||||
WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
|
||||
console.log('Jitsi widget added');
|
||||
}).catch((e) => {
|
||||
if (e.errcode === 'M_FORBIDDEN') {
|
||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||
|
||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||
title: _t('Permission Required'),
|
||||
description: _t("You do not have permission to start a conference call in this room"),
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
// FIXME: Nasty way of making sure we only register
|
||||
// with the dispatcher once
|
||||
if (!global.mxCallHandler) {
|
||||
dis.register(_onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
// end up causing the audio elements with our ring/ringback etc
|
||||
// audio clips in to play.
|
||||
if (navigator.mediaSession) {
|
||||
navigator.mediaSession.setActionHandler('play', function() {});
|
||||
navigator.mediaSession.setActionHandler('pause', function() {});
|
||||
navigator.mediaSession.setActionHandler('seekbackward', function() {});
|
||||
navigator.mediaSession.setActionHandler('seekforward', function() {});
|
||||
navigator.mediaSession.setActionHandler('previoustrack', function() {});
|
||||
navigator.mediaSession.setActionHandler('nexttrack', function() {});
|
||||
}
|
||||
}
|
||||
|
||||
const callHandler = {
|
||||
getCallForRoom: function(roomId) {
|
||||
let call = callHandler.getCall(roomId);
|
||||
if (call) return call;
|
||||
|
||||
if (ConferenceHandler) {
|
||||
call = ConferenceHandler.getConferenceCallForRoom(roomId);
|
||||
}
|
||||
if (call) return call;
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
getCall: function(roomId) {
|
||||
return calls[roomId] || null;
|
||||
},
|
||||
|
||||
getAnyActiveCall: function() {
|
||||
const roomsWithCalls = Object.keys(calls);
|
||||
for (let i = 0; i < roomsWithCalls.length; i++) {
|
||||
if (calls[roomsWithCalls[i]] &&
|
||||
calls[roomsWithCalls[i]].call_state !== "ended") {
|
||||
return calls[roomsWithCalls[i]];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* The conference handler is a module that deals with implementation-specific
|
||||
* multi-party calling implementations. Element passes in its own which creates
|
||||
* a one-to-one call with a freeswitch conference bridge. As of July 2018,
|
||||
* the de-facto way of conference calling is a Jitsi widget, so this is
|
||||
* deprecated. It reamins here for two reasons:
|
||||
* 1. So Element still supports joining existing freeswitch conference calls
|
||||
* (but doesn't support creating them). After a transition period, we can
|
||||
* remove support for joining them too.
|
||||
* 2. To hide the one-to-one rooms that old-style conferencing creates. This
|
||||
* is much harder to remove: probably either we make Element leave & forget these
|
||||
* rooms after we remove support for joining freeswitch conferences, or we
|
||||
* accept that random rooms with cryptic users will suddently appear for
|
||||
* anyone who's ever used conference calling, or we are stuck with this
|
||||
* code forever.
|
||||
*
|
||||
* @param {object} confHandler The conference handler object
|
||||
*/
|
||||
setConferenceHandler: function(confHandler) {
|
||||
ConferenceHandler = confHandler;
|
||||
},
|
||||
|
||||
getConferenceHandler: function() {
|
||||
return ConferenceHandler;
|
||||
},
|
||||
};
|
||||
// Only things in here which actually need to be global are the
|
||||
// calls list (done separately) and making sure we only register
|
||||
// with the dispatcher once (which uses this mechanism but checks
|
||||
// separately). This could be tidied up.
|
||||
if (global.mxCallHandler === undefined) {
|
||||
global.mxCallHandler = callHandler;
|
||||
}
|
||||
|
||||
export default global.mxCallHandler;
|
565
src/CallHandler.tsx
Normal file
|
@ -0,0 +1,565 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2018 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Manages a list of all the currently active calls.
|
||||
*
|
||||
* This handler dispatches when voip calls are added/updated/removed from this list:
|
||||
* {
|
||||
* action: 'call_state'
|
||||
* room_id: <room ID of the call>
|
||||
* }
|
||||
*
|
||||
* To know the state of the call, this handler exposes a getter to
|
||||
* obtain the call for a room:
|
||||
* var call = CallHandler.getCall(roomId)
|
||||
* var state = call.call_state; // ringing|ringback|connected|ended|busy|stop_ringback|stop_ringing
|
||||
*
|
||||
* This handler listens for and handles the following actions:
|
||||
* {
|
||||
* action: 'place_call',
|
||||
* type: 'voice|video',
|
||||
* room_id: <room that the place call button was pressed in>
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* action: 'incoming_call'
|
||||
* call: MatrixCall
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* action: 'hangup'
|
||||
* room_id: <room that the hangup button was pressed in>
|
||||
* }
|
||||
*
|
||||
* {
|
||||
* action: 'answer'
|
||||
* room_id: <room that the answer button was pressed in>
|
||||
* }
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import {generateHumanReadableId} from "./utils/NamingUtils";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {WidgetType} from "./widgets/WidgetType";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import { ActionPayload } from "./dispatcher/payloads";
|
||||
import {base32} from "rfc4648";
|
||||
|
||||
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
|
||||
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||
import WidgetStore from "./stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/lib/webrtc/call";
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
Ringback = 'ringbackAudio',
|
||||
CallEnd = 'callendAudio',
|
||||
Busy = 'busyAudio',
|
||||
}
|
||||
|
||||
// Unlike 'CallType' in js-sdk, this one includes screen sharing
|
||||
// (because a screen sharing call is only a screen sharing call to the caller,
|
||||
// to the callee it's just a video call, at least as far as the current impl
|
||||
// is concerned).
|
||||
export enum PlaceCallType {
|
||||
Voice = 'voice',
|
||||
Video = 'video',
|
||||
ScreenSharing = 'screensharing',
|
||||
}
|
||||
|
||||
export default class CallHandler {
|
||||
private calls = new Map<string, MatrixCall>();
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
|
||||
static sharedInstance() {
|
||||
if (!window.mxCallHandler) {
|
||||
window.mxCallHandler = new CallHandler()
|
||||
}
|
||||
|
||||
return window.mxCallHandler;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
dis.register(this.onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
// end up causing the audio elements with our ring/ringback etc
|
||||
// audio clips in to play.
|
||||
if (navigator.mediaSession) {
|
||||
navigator.mediaSession.setActionHandler('play', function() {});
|
||||
navigator.mediaSession.setActionHandler('pause', function() {});
|
||||
navigator.mediaSession.setActionHandler('seekbackward', function() {});
|
||||
navigator.mediaSession.setActionHandler('seekforward', function() {});
|
||||
navigator.mediaSession.setActionHandler('previoustrack', function() {});
|
||||
navigator.mediaSession.setActionHandler('nexttrack', function() {});
|
||||
}
|
||||
}
|
||||
|
||||
getCallForRoom(roomId: string): MatrixCall {
|
||||
return this.calls.get(roomId) || null;
|
||||
}
|
||||
|
||||
getAnyActiveCall() {
|
||||
for (const call of this.calls.values()) {
|
||||
if (call.state !== CallState.Ended) {
|
||||
return call;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
play(audioId: AudioID) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||
if (audio) {
|
||||
const playAudio = async () => {
|
||||
try {
|
||||
// This still causes the chrome debugger to break on promise rejection if
|
||||
// the promise is rejected, even though we're catching the exception.
|
||||
await audio.play();
|
||||
} catch (e) {
|
||||
// This is usually because the user hasn't interacted with the document,
|
||||
// or chrome doesn't think so and is denying the request. Not sure what
|
||||
// we can really do here...
|
||||
// https://github.com/vector-im/element-web/issues/7657
|
||||
console.log("Unable to play audio clip", e);
|
||||
}
|
||||
};
|
||||
if (this.audioPromises.has(audioId)) {
|
||||
this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => {
|
||||
audio.load();
|
||||
return playAudio();
|
||||
}));
|
||||
} else {
|
||||
this.audioPromises.set(audioId, playAudio());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pause(audioId: AudioID) {
|
||||
// TODO: Attach an invisible element for this instead
|
||||
// which listens?
|
||||
const audio = document.getElementById(audioId) as HTMLMediaElement;
|
||||
if (audio) {
|
||||
if (this.audioPromises.has(audioId)) {
|
||||
this.audioPromises.set(audioId, this.audioPromises.get(audioId).then(() => audio.pause()));
|
||||
} else {
|
||||
// pause doesn't return a promise, so just do it
|
||||
audio.pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private matchesCallForThisRoom(call: MatrixCall) {
|
||||
// We don't allow placing more than one call per room, but that doesn't mean there
|
||||
// can't be more than one, eg. in a glare situation. This checks that the given call
|
||||
// is the call we consider 'the' call for its room.
|
||||
const callForThisRoom = this.getCallForRoom(call.roomId);
|
||||
return callForThisRoom && call.callId === callForThisRoom.callId;
|
||||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
call.on(CallEvent.Error, (err) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
console.error("Call error:", err);
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
SettingsStore.getValue("fallbackICEServerAllowed") === null
|
||||
) {
|
||||
this.showICEFallbackPrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||
title: _t('Call Failed'),
|
||||
description: err.message,
|
||||
});
|
||||
});
|
||||
call.on(CallEvent.Hangup, () => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
this.removeCallForRoom(call.roomId);
|
||||
});
|
||||
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
this.setCallState(call, newState);
|
||||
|
||||
switch (oldState) {
|
||||
case CallState.Ringing:
|
||||
this.pause(AudioID.Ring);
|
||||
break;
|
||||
case CallState.InviteSent:
|
||||
this.pause(AudioID.Ringback);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (newState) {
|
||||
case CallState.Ringing:
|
||||
this.play(AudioID.Ring);
|
||||
break;
|
||||
case CallState.InviteSent:
|
||||
this.play(AudioID.Ringback);
|
||||
break;
|
||||
case CallState.Ended:
|
||||
this.removeCallForRoom(call.roomId);
|
||||
if (oldState === CallState.InviteSent && (
|
||||
call.hangupParty === CallParty.Remote ||
|
||||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||
)) {
|
||||
this.play(AudioID.Busy);
|
||||
let title;
|
||||
let description;
|
||||
if (call.hangupReason === CallErrorCode.UserHangup) {
|
||||
title = _t("Call Declined");
|
||||
description = _t("The other party declined the call.");
|
||||
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
||||
title = _t("Call Failed");
|
||||
// XXX: full stop appended as some relic here, but these
|
||||
// strings need proper input from design anyway, so let's
|
||||
// not change this string until we have a proper one.
|
||||
description = _t('The remote side failed to pick up') + '.';
|
||||
} else {
|
||||
title = _t("Call Failed");
|
||||
description = _t("The call could not be established");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title, description,
|
||||
});
|
||||
} else {
|
||||
this.play(AudioID.CallEnd);
|
||||
}
|
||||
}
|
||||
});
|
||||
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
|
||||
|
||||
if (call.state === CallState.Ringing) {
|
||||
this.pause(AudioID.Ring);
|
||||
} else if (call.state === CallState.InviteSent) {
|
||||
this.pause(AudioID.Ringback);
|
||||
}
|
||||
|
||||
this.calls.set(newCall.roomId, newCall);
|
||||
this.setCallListeners(newCall);
|
||||
this.setCallState(newCall, newCall.state);
|
||||
});
|
||||
}
|
||||
|
||||
private setCallState(call: MatrixCall, status: CallState) {
|
||||
console.log(
|
||||
`Call state in ${call.roomId} changed to ${status}`,
|
||||
);
|
||||
|
||||
dis.dispatch({
|
||||
action: 'call_state',
|
||||
room_id: call.roomId,
|
||||
state: status,
|
||||
});
|
||||
}
|
||||
|
||||
private removeCallForRoom(roomId: string) {
|
||||
this.calls.delete(roomId);
|
||||
}
|
||||
|
||||
private showICEFallbackPrompt() {
|
||||
const cli = MatrixClientPeg.get();
|
||||
const code = sub => <code>{sub}</code>;
|
||||
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
|
||||
title: _t("Call failed due to misconfigured server"),
|
||||
description: <div>
|
||||
<p>{_t(
|
||||
"Please ask the administrator of your homeserver " +
|
||||
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
|
||||
"order for calls to work reliably.",
|
||||
{ homeserverDomain: cli.getDomain() }, { code },
|
||||
)}</p>
|
||||
<p>{_t(
|
||||
"Alternatively, you can try to use the public server at " +
|
||||
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
|
||||
"it will share your IP address with that server. You can also manage " +
|
||||
"this in Settings.",
|
||||
null, { code },
|
||||
)}</p>
|
||||
</div>,
|
||||
button: _t('Try using turn.matrix.org'),
|
||||
cancelButton: _t('OK'),
|
||||
onFinished: (allow) => {
|
||||
SettingsStore.setValue("fallbackICEServerAllowed", null, SettingLevel.DEVICE, allow);
|
||||
cli.setFallbackICEServerAllowed(allow);
|
||||
},
|
||||
}, null, true);
|
||||
}
|
||||
|
||||
|
||||
private placeCall(
|
||||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
) {
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
||||
this.calls.set(roomId, call);
|
||||
this.setCallListeners(call);
|
||||
if (type === PlaceCallType.Voice) {
|
||||
call.placeVoiceCall();
|
||||
} else if (type === 'video') {
|
||||
call.placeVideoCall(
|
||||
remoteElement,
|
||||
localElement,
|
||||
);
|
||||
} else if (type === PlaceCallType.ScreenSharing) {
|
||||
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
|
||||
if (screenCapErrorString) {
|
||||
this.removeCallForRoom(roomId);
|
||||
console.log("Can't capture screen: " + screenCapErrorString);
|
||||
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
|
||||
title: _t('Unable to capture screen'),
|
||||
description: screenCapErrorString,
|
||||
});
|
||||
return;
|
||||
}
|
||||
call.placeScreenSharingCall(remoteElement, localElement);
|
||||
} else {
|
||||
console.error("Unknown conf call type: %s", type);
|
||||
}
|
||||
}
|
||||
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
switch (payload.action) {
|
||||
case 'place_call':
|
||||
{
|
||||
if (this.getAnyActiveCall()) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
|
||||
title: _t('Existing Call'),
|
||||
description: _t('You are already in a call.'),
|
||||
});
|
||||
return; // don't allow >1 call to be placed.
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, whine.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
Modal.createTrackedDialog('Call Handler', 'VoIP is unsupported', ErrorDialog, {
|
||||
title: _t('VoIP is unsupported'),
|
||||
description: _t('You cannot place VoIP calls in this browser.'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const room = MatrixClientPeg.get().getRoom(payload.room_id);
|
||||
if (!room) {
|
||||
console.error("Room %s does not exist.", payload.room_id);
|
||||
return;
|
||||
}
|
||||
|
||||
const members = room.getJoinedMembers();
|
||||
if (members.length <= 1) {
|
||||
Modal.createTrackedDialog('Call Handler', 'Cannot place call with self', ErrorDialog, {
|
||||
description: _t('You cannot place a call with yourself.'),
|
||||
});
|
||||
return;
|
||||
} else if (members.length === 2) {
|
||||
console.info("Place %s call in %s", payload.type, payload.room_id);
|
||||
|
||||
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
|
||||
} else { // > 2
|
||||
dis.dispatch({
|
||||
action: "place_conference_call",
|
||||
room_id: payload.room_id,
|
||||
type: payload.type,
|
||||
remote_element: payload.remote_element,
|
||||
local_element: payload.local_element,
|
||||
});
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'place_conference_call':
|
||||
console.info("Place conference call in %s", payload.room_id);
|
||||
this.startCallApp(payload.room_id, payload.type);
|
||||
break;
|
||||
case 'end_conference':
|
||||
console.info("Terminating conference call in %s", payload.room_id);
|
||||
this.terminateCallApp(payload.room_id);
|
||||
break;
|
||||
case 'hangup_conference':
|
||||
console.info("Leaving conference call in %s", payload.room_id);
|
||||
this.hangupCallApp(payload.room_id);
|
||||
break;
|
||||
case 'incoming_call':
|
||||
{
|
||||
if (this.getAnyActiveCall()) {
|
||||
// ignore multiple incoming calls. in future, we may want a line-1/line-2 setup.
|
||||
// we avoid rejecting with "busy" in case the user wants to answer it on a different device.
|
||||
// in future we could signal a "local busy" as a warning to the caller.
|
||||
// see https://github.com/vector-im/vector-web/issues/1964
|
||||
return;
|
||||
}
|
||||
|
||||
// if the runtime env doesn't do VoIP, stop here.
|
||||
if (!MatrixClientPeg.get().supportsVoip()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const call = payload.call as MatrixCall;
|
||||
this.calls.set(call.roomId, call)
|
||||
this.setCallListeners(call);
|
||||
}
|
||||
break;
|
||||
case 'hangup':
|
||||
case 'reject':
|
||||
if (!this.calls.get(payload.room_id)) {
|
||||
return; // no call to hangup
|
||||
}
|
||||
if (payload.action === 'reject') {
|
||||
this.calls.get(payload.room_id).reject();
|
||||
} else {
|
||||
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
|
||||
}
|
||||
this.removeCallForRoom(payload.room_id);
|
||||
break;
|
||||
case 'answer':
|
||||
if (!this.calls.has(payload.room_id)) {
|
||||
return; // no call to answer
|
||||
}
|
||||
this.calls.get(payload.room_id).answer();
|
||||
dis.dispatch({
|
||||
action: "view_room",
|
||||
room_id: payload.room_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private async startCallApp(roomId: string, type: string) {
|
||||
dis.dispatch({
|
||||
action: 'appsDrawer',
|
||||
show: true,
|
||||
});
|
||||
|
||||
// prevent double clicking the call button
|
||||
const room = MatrixClientPeg.get().getRoom(roomId);
|
||||
const currentJitsiWidgets = WidgetUtils.getRoomWidgetsOfType(room, WidgetType.JITSI);
|
||||
const hasJitsi = currentJitsiWidgets.length > 0
|
||||
|| WidgetEchoStore.roomHasPendingWidgetsOfType(roomId, currentJitsiWidgets, WidgetType.JITSI);
|
||||
if (hasJitsi) {
|
||||
Modal.createTrackedDialog('Call already in progress', '', ErrorDialog, {
|
||||
title: _t('Call in Progress'),
|
||||
description: _t('A call is currently being placed!'),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const jitsiDomain = Jitsi.getInstance().preferredDomain;
|
||||
const jitsiAuth = await Jitsi.getInstance().getJitsiAuth();
|
||||
let confId;
|
||||
if (jitsiAuth === 'openidtoken-jwt') {
|
||||
// Create conference ID from room ID
|
||||
// For compatibility with Jitsi, use base32 without padding.
|
||||
// More details here:
|
||||
// https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
|
||||
confId = base32.stringify(Buffer.from(roomId), { pad: false });
|
||||
} else {
|
||||
// Create a random human readable conference ID
|
||||
confId = `JitsiConference${generateHumanReadableId()}`;
|
||||
}
|
||||
|
||||
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
|
||||
|
||||
// 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,
|
||||
auth: jitsiAuth,
|
||||
};
|
||||
|
||||
const widgetId = (
|
||||
'jitsi_' +
|
||||
MatrixClientPeg.get().credentials.userId +
|
||||
'_' +
|
||||
Date.now()
|
||||
);
|
||||
|
||||
WidgetUtils.setRoomWidget(roomId, widgetId, WidgetType.JITSI, widgetUrl, 'Jitsi', widgetData).then(() => {
|
||||
console.log('Jitsi widget added');
|
||||
}).catch((e) => {
|
||||
if (e.errcode === 'M_FORBIDDEN') {
|
||||
Modal.createTrackedDialog('Call Failed', '', ErrorDialog, {
|
||||
title: _t('Permission Required'),
|
||||
description: _t("You do not have permission to start a conference call in this room"),
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
});
|
||||
}
|
||||
|
||||
private terminateCallApp(roomId: string) {
|
||||
Modal.createTrackedDialog('Confirm Jitsi Terminate', '', QuestionDialog, {
|
||||
hasCancelButton: true,
|
||||
title: _t("End conference"),
|
||||
description: _t("This will end the conference for everyone. Continue?"),
|
||||
button: _t("End conference"),
|
||||
onFinished: (proceed) => {
|
||||
if (!proceed) return;
|
||||
|
||||
// We'll just obliterate them all. There should only ever be one, but might as well
|
||||
// be safe.
|
||||
const roomInfo = WidgetStore.instance.getRoom(roomId);
|
||||
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
|
||||
jitsiWidgets.forEach(w => {
|
||||
// setting invalid content removes it
|
||||
WidgetUtils.setRoomWidget(roomId, w.id);
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private hangupCallApp(roomId: string) {
|
||||
const roomInfo = WidgetStore.instance.getRoom(roomId);
|
||||
if (!roomInfo) return; // "should never happen" clauses go here
|
||||
|
||||
const jitsiWidgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type));
|
||||
jitsiWidgets.forEach(w => {
|
||||
const messaging = WidgetMessagingStore.instance.getMessagingForId(w.id);
|
||||
if (!messaging) return; // more "should never happen" words
|
||||
|
||||
messaging.transport.send(ElementWidgetActions.HangupCall, {});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import extend from './extend';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
@ -70,6 +69,7 @@ interface IContent {
|
|||
|
||||
interface IThumbnail {
|
||||
info: {
|
||||
// eslint-disable-next-line camelcase
|
||||
thumbnail_info: {
|
||||
w: number;
|
||||
h: number;
|
||||
|
@ -104,7 +104,12 @@ interface IAbortablePromise<T> extends Promise<T> {
|
|||
* @return {Promise} A promise that resolves with an object with an info key
|
||||
* and a thumbnail key.
|
||||
*/
|
||||
function createThumbnail(element: ThumbnailableElement, inputWidth: number, inputHeight: number, mimeType: string): Promise<IThumbnail> {
|
||||
function createThumbnail(
|
||||
element: ThumbnailableElement,
|
||||
inputWidth: number,
|
||||
inputHeight: number,
|
||||
mimeType: string,
|
||||
): Promise<IThumbnail> {
|
||||
return new Promise((resolve) => {
|
||||
let targetWidth = inputWidth;
|
||||
let targetHeight = inputHeight;
|
||||
|
@ -437,11 +442,13 @@ export default class ContentMessages {
|
|||
for (let i = 0; i < okFiles.length; ++i) {
|
||||
const file = okFiles[i];
|
||||
if (!uploadAll) {
|
||||
const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, {
|
||||
const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation',
|
||||
'', UploadConfirmDialog, {
|
||||
file,
|
||||
currentIndex: i,
|
||||
totalFiles: okFiles.length,
|
||||
});
|
||||
},
|
||||
);
|
||||
const [shouldContinue, shouldUploadAll] = await finished;
|
||||
if (!shouldContinue) break;
|
||||
if (shouldUploadAll) {
|
||||
|
@ -489,7 +496,7 @@ export default class ContentMessages {
|
|||
if (file.type.indexOf('image/') === 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
||||
extend(content.info, imageInfo);
|
||||
Object.assign(content.info, imageInfo);
|
||||
resolve();
|
||||
}, (e) => {
|
||||
console.error(e);
|
||||
|
@ -502,7 +509,7 @@ export default class ContentMessages {
|
|||
} else if (file.type.indexOf('video/') === 0) {
|
||||
content.msgtype = 'm.video';
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
|
||||
extend(content.info, videoInfo);
|
||||
Object.assign(content.info, videoInfo);
|
||||
resolve();
|
||||
}, (e) => {
|
||||
content.msgtype = 'm.file';
|
||||
|
|
|
@ -1,248 +0,0 @@
|
|||
/*
|
||||
Copyright 2019 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 Modal from './Modal';
|
||||
import * as sdk from './index';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||
import { _t } from './languageHandler';
|
||||
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
|
||||
// during the same single operation. Use `accessSecretStorage` below to scope a
|
||||
// single secret storage operation, as it will clear the cached keys once the
|
||||
// operation ends.
|
||||
let secretStorageKeys = {};
|
||||
let secretStorageBeingAccessed = false;
|
||||
|
||||
function isCachingAllowed() {
|
||||
return secretStorageBeingAccessed;
|
||||
}
|
||||
|
||||
export class AccessCancelledError extends Error {
|
||||
constructor() {
|
||||
super("Secret storage access canceled");
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmToDismiss() {
|
||||
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
|
||||
const [sure] = await Modal.createDialog(QuestionDialog, {
|
||||
title: _t("Cancel entering passphrase?"),
|
||||
description: _t("Are you sure you want to cancel entering passphrase?"),
|
||||
danger: false,
|
||||
button: _t("Go Back"),
|
||||
cancelButton: _t("Cancel"),
|
||||
}).finished;
|
||||
return !sure;
|
||||
}
|
||||
|
||||
async function getSecretStorageKey({ keys: keyInfos }, ssssItemName) {
|
||||
const keyInfoEntries = Object.entries(keyInfos);
|
||||
if (keyInfoEntries.length > 1) {
|
||||
throw new Error("Multiple storage key requests not implemented");
|
||||
}
|
||||
const [name, info] = keyInfoEntries[0];
|
||||
|
||||
// Check the in-memory cache
|
||||
if (isCachingAllowed() && secretStorageKeys[name]) {
|
||||
return [name, secretStorageKeys[name]];
|
||||
}
|
||||
|
||||
const inputToKey = async ({ passphrase, recoveryKey }) => {
|
||||
if (passphrase) {
|
||||
return deriveKey(
|
||||
passphrase,
|
||||
info.passphrase.salt,
|
||||
info.passphrase.iterations,
|
||||
);
|
||||
} else {
|
||||
return decodeRecoveryKey(recoveryKey);
|
||||
}
|
||||
};
|
||||
const AccessSecretStorageDialog =
|
||||
sdk.getComponent("dialogs.secretstorage.AccessSecretStorageDialog");
|
||||
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
||||
AccessSecretStorageDialog,
|
||||
/* props= */
|
||||
{
|
||||
keyInfo: info,
|
||||
checkPrivateKey: async (input) => {
|
||||
const key = await inputToKey(input);
|
||||
return await MatrixClientPeg.get().checkSecretStorageKey(key, info);
|
||||
},
|
||||
},
|
||||
/* className= */ null,
|
||||
/* isPriorityModal= */ false,
|
||||
/* isStaticModal= */ false,
|
||||
/* options= */ {
|
||||
onBeforeClose: async (reason) => {
|
||||
if (reason === "backgroundClick") {
|
||||
return confirmToDismiss();
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
);
|
||||
const [input] = await finished;
|
||||
if (!input) {
|
||||
throw new AccessCancelledError();
|
||||
}
|
||||
const key = await inputToKey(input);
|
||||
|
||||
// Save to cache to avoid future prompts in the current session
|
||||
if (isCachingAllowed()) {
|
||||
secretStorageKeys[name] = key;
|
||||
}
|
||||
|
||||
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 === "m.cross_signing.master" ||
|
||||
name === "m.cross_signing.self_signing" ||
|
||||
name === "m.cross_signing.user_signing"
|
||||
) {
|
||||
const callbacks = client.getCrossSigningCacheCallbacks();
|
||||
if (!callbacks.getCrossSigningKeyCache) return;
|
||||
const keyId = name.replace("m.cross_signing.", "");
|
||||
const key = await callbacks.getCrossSigningKeyCache(keyId);
|
||||
if (!key) {
|
||||
console.log(
|
||||
`${keyId} 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
|
||||
* each other in a cycle of sorts) have been bootstrapped before running the
|
||||
* provided function.
|
||||
*
|
||||
* Bootstrapping secret storage may take one of these paths:
|
||||
* 1. Create secret storage from a passphrase and store cross-signing keys
|
||||
* in secret storage.
|
||||
* 2. Access existing secret storage by requesting passphrase and accessing
|
||||
* cross-signing keys as needed.
|
||||
* 3. All keys are loaded and there's nothing to do.
|
||||
*
|
||||
* Additionally, the secret storage keys are cached during the scope of this function
|
||||
* to ensure the user is prompted only once for their secret storage
|
||||
* passphrase. The cache is then cleared once the provided function completes.
|
||||
*
|
||||
* @param {Function} [func] An operation to perform once secret storage has been
|
||||
* bootstrapped. Optional.
|
||||
* @param {bool} [forceReset] Reset secret storage even if it's already set up
|
||||
*/
|
||||
export async function accessSecretStorage(func = async () => { }, forceReset = false) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
secretStorageBeingAccessed = true;
|
||||
try {
|
||||
if (!await cli.hasSecretStorageKey() || forceReset) {
|
||||
// This dialog calls bootstrap itself after guiding the user through
|
||||
// passphrase creation.
|
||||
const { finished } = Modal.createTrackedDialogAsync('Create Secret Storage dialog', '',
|
||||
import("./async-components/views/dialogs/secretstorage/CreateSecretStorageDialog"),
|
||||
{
|
||||
force: forceReset,
|
||||
},
|
||||
null, /* priority = */ false, /* static = */ true,
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Secret storage creation canceled");
|
||||
}
|
||||
} else {
|
||||
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
|
||||
await cli.bootstrapSecretStorage({
|
||||
authUploadDeviceSigningKeys: async (makeRequest) => {
|
||||
const { finished } = Modal.createTrackedDialog(
|
||||
'Cross-signing keys dialog', '', InteractiveAuthDialog,
|
||||
{
|
||||
title: _t("Setting up keys"),
|
||||
matrixClient: MatrixClientPeg.get(),
|
||||
makeRequest,
|
||||
},
|
||||
);
|
||||
const [confirmed] = await finished;
|
||||
if (!confirmed) {
|
||||
throw new Error("Cross-signing key upload auth canceled");
|
||||
}
|
||||
},
|
||||
getBackupPassphrase: promptForBackupPassphrase,
|
||||
});
|
||||
}
|
||||
|
||||
// `return await` needed here to ensure `finally` block runs after the
|
||||
// inner operation completes.
|
||||
return await func();
|
||||
} finally {
|
||||
// Clear secret storage key cache now that work is complete
|
||||
secretStorageBeingAccessed = false;
|
||||
if (!isCachingAllowed()) {
|
||||
secretStorageKeys = {};
|
||||
}
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
function getDaysArray() {
|
||||
function getDaysArray(): string[] {
|
||||
return [
|
||||
_t('Sun'),
|
||||
_t('Mon'),
|
||||
|
@ -29,7 +29,7 @@ function getDaysArray() {
|
|||
];
|
||||
}
|
||||
|
||||
function getMonthsArray() {
|
||||
function getMonthsArray(): string[] {
|
||||
return [
|
||||
_t('Jan'),
|
||||
_t('Feb'),
|
||||
|
@ -46,11 +46,11 @@ function getMonthsArray() {
|
|||
];
|
||||
}
|
||||
|
||||
function pad(n) {
|
||||
function pad(n: number): string {
|
||||
return (n < 10 ? '0' : '') + n;
|
||||
}
|
||||
|
||||
function twelveHourTime(date, showSeconds=false) {
|
||||
function twelveHourTime(date: Date, showSeconds = false): string {
|
||||
let hours = date.getHours() % 12;
|
||||
const minutes = pad(date.getMinutes());
|
||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||
|
@ -62,7 +62,7 @@ function twelveHourTime(date, showSeconds=false) {
|
|||
return `${hours}:${minutes}${ampm}`;
|
||||
}
|
||||
|
||||
export function formatDate(date, showTwelveHour=false) {
|
||||
export function formatDate(date: Date, showTwelveHour = false): string {
|
||||
const now = new Date();
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
|
@ -86,7 +86,7 @@ export function formatDate(date, showTwelveHour=false) {
|
|||
return formatFullDate(date, showTwelveHour);
|
||||
}
|
||||
|
||||
export function formatFullDateNoTime(date) {
|
||||
export function formatFullDateNoTime(date: Date): string {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
|
||||
|
@ -97,7 +97,7 @@ export function formatFullDateNoTime(date) {
|
|||
});
|
||||
}
|
||||
|
||||
export function formatFullDate(date, showTwelveHour=false) {
|
||||
export function formatFullDate(date: Date, showTwelveHour = false): string {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
||||
|
@ -109,14 +109,14 @@ export function formatFullDate(date, showTwelveHour=false) {
|
|||
});
|
||||
}
|
||||
|
||||
export function formatFullTime(date, showTwelveHour=false) {
|
||||
export function formatFullTime(date: Date, showTwelveHour = false): string {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date, true);
|
||||
}
|
||||
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
}
|
||||
|
||||
export function formatTime(date, showTwelveHour=false) {
|
||||
export function formatTime(date: Date, showTwelveHour = false): string {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date);
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ export function formatTime(date, showTwelveHour=false) {
|
|||
}
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
export function wantsDateSeparator(prevEventDate, nextEventDate) {
|
||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
|
@ -15,6 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import {
|
||||
hideToast as hideBulkUnverifiedSessionsToast,
|
||||
showToast as showBulkUnverifiedSessionsToast,
|
||||
|
@ -28,11 +29,15 @@ import {
|
|||
hideToast as hideUnverifiedSessionsToast,
|
||||
showToast as showUnverifiedSessionsToast,
|
||||
} from "./toasts/UnverifiedSessionToast";
|
||||
import {privateShouldBeEncrypted} from "./createRoom";
|
||||
import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityManager";
|
||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||
import { isLoggedIn } from './components/structures/MatrixChat';
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
|
||||
|
||||
export default class DeviceListener {
|
||||
private dispatcherRef: string;
|
||||
// device IDs for which the user has dismissed the verify toast ('Later')
|
||||
private dismissed = new Set<string>();
|
||||
// has the user dismissed any of the various nag toasts to setup encryption on this device?
|
||||
|
@ -60,6 +65,8 @@ export default class DeviceListener {
|
|||
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().on('accountData', this._onAccountData);
|
||||
MatrixClientPeg.get().on('sync', this._onSync);
|
||||
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
|
||||
this.dispatcherRef = dis.register(this._onAction);
|
||||
this._recheck();
|
||||
}
|
||||
|
||||
|
@ -72,6 +79,11 @@ export default class DeviceListener {
|
|||
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
|
||||
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
|
||||
MatrixClientPeg.get().removeListener('sync', this._onSync);
|
||||
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
|
||||
}
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.dispatcherRef = null;
|
||||
}
|
||||
this.dismissed.clear();
|
||||
this.dismissedThisDeviceToast = false;
|
||||
|
@ -158,6 +170,21 @@ export default class DeviceListener {
|
|||
if (state === 'PREPARED' && prevState === null) this._recheck();
|
||||
};
|
||||
|
||||
_onRoomStateEvents = (ev: MatrixEvent) => {
|
||||
if (ev.getType() !== "m.room.encryption") {
|
||||
return;
|
||||
}
|
||||
|
||||
// If a room changes to encrypted, re-check as it may be our first
|
||||
// encrypted room. This also catches encrypted room creation as well.
|
||||
this._recheck();
|
||||
};
|
||||
|
||||
_onAction = ({ action }) => {
|
||||
if (action !== "on_logged_in") return;
|
||||
this._recheck();
|
||||
};
|
||||
|
||||
// The server doesn't tell us when key backup is set up, so we poll
|
||||
// & cache the result
|
||||
async _getKeyBackupInfo() {
|
||||
|
@ -170,9 +197,10 @@ export default class DeviceListener {
|
|||
}
|
||||
|
||||
private shouldShowSetupEncryptionToast() {
|
||||
// In a default configuration, show the toasts. If the well-known config causes e2ee default to be false
|
||||
// then do not show the toasts until user is in at least one encrypted room.
|
||||
if (privateShouldBeEncrypted()) return true;
|
||||
// If we're in the middle of a secret storage operation, we're likely
|
||||
// modifying the state involved here, so don't add new toasts to setup.
|
||||
if (isSecretStorageBeingAccessed()) return false;
|
||||
// Show setup toasts once the user is in at least one encrypted room.
|
||||
const cli = MatrixClientPeg.get();
|
||||
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
|
||||
}
|
||||
|
@ -189,15 +217,20 @@ export default class DeviceListener {
|
|||
if (!cli.isInitialSyncComplete()) return;
|
||||
|
||||
const crossSigningReady = await cli.isCrossSigningReady();
|
||||
const secretStorageReady = await cli.isSecretStorageReady();
|
||||
const allSystemsReady = crossSigningReady && secretStorageReady;
|
||||
|
||||
if (this.dismissedThisDeviceToast || crossSigningReady) {
|
||||
if (this.dismissedThisDeviceToast || allSystemsReady) {
|
||||
hideSetupEncryptionToast();
|
||||
} else if (this.shouldShowSetupEncryptionToast()) {
|
||||
// make sure our keys are finished downloading
|
||||
await cli.downloadKeys([cli.getUserId()]);
|
||||
// cross signing isn't enabled - nag to enable it
|
||||
// There are 3 different toasts for:
|
||||
if (cli.getStoredCrossSigningForUser(cli.getUserId())) {
|
||||
if (
|
||||
!cli.getCrossSigningId() &&
|
||||
cli.getStoredCrossSigningForUser(cli.getUserId())
|
||||
) {
|
||||
// Cross-signing on account but this device doesn't trust the master key (verify this session)
|
||||
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
|
||||
} else {
|
||||
|
@ -207,10 +240,18 @@ export default class DeviceListener {
|
|||
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
|
||||
} else {
|
||||
// No cross-signing or key backup on account (set up encryption)
|
||||
await cli.waitForClientWellKnown();
|
||||
if (isSecureBackupRequired() && isLoggedIn()) {
|
||||
// If we're meant to set up, and Secure Backup is required,
|
||||
// trigger the flow directly without a toast once logged in.
|
||||
hideSetupEncryptionToast();
|
||||
accessSecretStorage();
|
||||
} else {
|
||||
showSetupEncryptionToast(SetupKind.SET_UP_ENCRYPTION);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This needs to be done after awaiting on downloadKeys() above, so
|
||||
// we make sure we get the devices after the fetch is done.
|
||||
|
|
|
@ -1,275 +0,0 @@
|
|||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 Travis Ralston
|
||||
Copyright 2019 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 URL from 'url';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetMessagingEndpoint from './WidgetMessagingEndpoint';
|
||||
import ActiveWidgetStore from './stores/ActiveWidgetStore';
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import {IntegrationManagers} from "./integrations/IntegrationManagers";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {Capability} from "./widgets/WidgetApi";
|
||||
import {objectClone} from "./utils/objects";
|
||||
|
||||
const WIDGET_API_VERSION = '0.0.2'; // Current API version
|
||||
const SUPPORTED_WIDGET_API_VERSIONS = [
|
||||
'0.0.1',
|
||||
'0.0.2',
|
||||
];
|
||||
const INBOUND_API_NAME = 'fromWidget';
|
||||
|
||||
// Listen for and handle incoming requests using the 'fromWidget' postMessage
|
||||
// API and initiate responses
|
||||
export default class FromWidgetPostMessageApi {
|
||||
constructor() {
|
||||
this.widgetMessagingEndpoints = [];
|
||||
this.widgetListeners = {}; // {action: func[]}
|
||||
|
||||
this.start = this.start.bind(this);
|
||||
this.stop = this.stop.bind(this);
|
||||
this.onPostMessage = this.onPostMessage.bind(this);
|
||||
}
|
||||
|
||||
start() {
|
||||
window.addEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
stop() {
|
||||
window.removeEventListener('message', this.onPostMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for a given action
|
||||
* @param {string} action The action to listen for.
|
||||
* @param {Function} callbackFn A callback function to be called when the action is
|
||||
* encountered. Called with two parameters: the interesting request information and
|
||||
* the raw event received from the postMessage API. The raw event is meant to be used
|
||||
* for sendResponse and similar functions.
|
||||
*/
|
||||
addListener(action, callbackFn) {
|
||||
if (!this.widgetListeners[action]) this.widgetListeners[action] = [];
|
||||
this.widgetListeners[action].push(callbackFn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a listener for a given action.
|
||||
* @param {string} action The action that was subscribed to.
|
||||
* @param {Function} callbackFn The original callback function that was used to subscribe
|
||||
* to updates.
|
||||
*/
|
||||
removeListener(action, callbackFn) {
|
||||
if (!this.widgetListeners[action]) return;
|
||||
|
||||
const idx = this.widgetListeners[action].indexOf(callbackFn);
|
||||
if (idx !== -1) this.widgetListeners[action].splice(idx, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a widget endpoint for trusted postMessage communication
|
||||
* @param {string} widgetId Unique widget identifier
|
||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||
*/
|
||||
addEndpoint(widgetId, endpointUrl) {
|
||||
const u = URL.parse(endpointUrl);
|
||||
if (!u || !u.protocol || !u.host) {
|
||||
console.warn('Add FromWidgetPostMessageApi endpoint - Invalid origin:', endpointUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = u.protocol + '//' + u.host;
|
||||
const endpoint = new WidgetMessagingEndpoint(widgetId, origin);
|
||||
if (this.widgetMessagingEndpoints.some(function(ep) {
|
||||
return (ep.widgetId === widgetId && ep.endpointUrl === endpointUrl);
|
||||
})) {
|
||||
// Message endpoint already registered
|
||||
console.warn('Add FromWidgetPostMessageApi - Endpoint already registered');
|
||||
return;
|
||||
} else {
|
||||
console.log(`Adding fromWidget messaging endpoint for ${widgetId}`, endpoint);
|
||||
this.widgetMessagingEndpoints.push(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* De-register a widget endpoint from trusted communication sources
|
||||
* @param {string} widgetId Unique widget identifier
|
||||
* @param {string} endpointUrl Widget wurl origin (protocol + (optional port) + host)
|
||||
* @return {boolean} True if endpoint was successfully removed
|
||||
*/
|
||||
removeEndpoint(widgetId, endpointUrl) {
|
||||
const u = URL.parse(endpointUrl);
|
||||
if (!u || !u.protocol || !u.host) {
|
||||
console.warn('Remove widget messaging endpoint - Invalid origin');
|
||||
return;
|
||||
}
|
||||
|
||||
const origin = u.protocol + '//' + u.host;
|
||||
if (this.widgetMessagingEndpoints && this.widgetMessagingEndpoints.length > 0) {
|
||||
const length = this.widgetMessagingEndpoints.length;
|
||||
this.widgetMessagingEndpoints = this.widgetMessagingEndpoints
|
||||
.filter((endpoint) => endpoint.widgetId !== widgetId || endpoint.endpointUrl !== origin);
|
||||
return (length > this.widgetMessagingEndpoints.length);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle widget postMessage events
|
||||
* Messages are only handled where a valid, registered messaging endpoints
|
||||
* @param {Event} event Event to handle
|
||||
* @return {undefined}
|
||||
*/
|
||||
onPostMessage(event) {
|
||||
if (!event.origin) { // Handle chrome
|
||||
event.origin = event.originalEvent.origin;
|
||||
}
|
||||
|
||||
// Event origin is empty string if undefined
|
||||
if (
|
||||
event.origin.length === 0 ||
|
||||
!this.trustedEndpoint(event.origin) ||
|
||||
event.data.api !== INBOUND_API_NAME ||
|
||||
!event.data.widgetId
|
||||
) {
|
||||
return; // don't log this - debugging APIs like to spam postMessage which floods the log otherwise
|
||||
}
|
||||
|
||||
// Call any listeners we have registered
|
||||
if (this.widgetListeners[event.data.action]) {
|
||||
for (const fn of this.widgetListeners[event.data.action]) {
|
||||
fn(event.data, event);
|
||||
}
|
||||
}
|
||||
|
||||
// Although the requestId is required, we don't use it. We'll be nice and process the message
|
||||
// if the property is missing, but with a warning for widget developers.
|
||||
if (!event.data.requestId) {
|
||||
console.warn("fromWidget action '" + event.data.action + "' does not have a requestId");
|
||||
}
|
||||
|
||||
const action = event.data.action;
|
||||
const widgetId = event.data.widgetId;
|
||||
if (action === 'content_loaded') {
|
||||
console.log('Widget reported content loaded for', widgetId);
|
||||
dis.dispatch({
|
||||
action: 'widget_content_loaded',
|
||||
widgetId: widgetId,
|
||||
});
|
||||
this.sendResponse(event, {success: true});
|
||||
} else if (action === 'supported_api_versions') {
|
||||
this.sendResponse(event, {
|
||||
api: INBOUND_API_NAME,
|
||||
supported_versions: SUPPORTED_WIDGET_API_VERSIONS,
|
||||
});
|
||||
} else if (action === 'api_version') {
|
||||
this.sendResponse(event, {
|
||||
api: INBOUND_API_NAME,
|
||||
version: WIDGET_API_VERSION,
|
||||
});
|
||||
} else if (action === 'm.sticker') {
|
||||
// console.warn('Got sticker message from widget', widgetId);
|
||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
||||
const data = event.data.data || event.data.widgetData;
|
||||
dis.dispatch({action: 'm.sticker', data: data, widgetId: event.data.widgetId});
|
||||
} else if (action === 'integration_manager_open') {
|
||||
// Close the stickerpicker
|
||||
dis.dispatch({action: 'stickerpicker_close'});
|
||||
// Open the integration manager
|
||||
// NOTE -- The widgetData field is deprecated (in favour of the 'data' field) and will be removed eventually
|
||||
const data = event.data.data || event.data.widgetData;
|
||||
const integType = (data && data.integType) ? data.integType : null;
|
||||
const integId = (data && data.integId) ? data.integId : null;
|
||||
|
||||
// TODO: Open the right integration manager for the widget
|
||||
if (SettingsStore.getValue("feature_many_integration_managers")) {
|
||||
IntegrationManagers.sharedInstance().openAll(
|
||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||
`type_${integType}`,
|
||||
integId,
|
||||
);
|
||||
} else {
|
||||
IntegrationManagers.sharedInstance().getPrimaryManager().open(
|
||||
MatrixClientPeg.get().getRoom(RoomViewStore.getRoomId()),
|
||||
`type_${integType}`,
|
||||
integId,
|
||||
);
|
||||
}
|
||||
} else if (action === 'set_always_on_screen') {
|
||||
// This is a new message: there is no reason to support the deprecated widgetData here
|
||||
const data = event.data.data;
|
||||
const val = data.value;
|
||||
|
||||
if (ActiveWidgetStore.widgetHasCapability(widgetId, Capability.AlwaysOnScreen)) {
|
||||
ActiveWidgetStore.setWidgetPersistence(widgetId, val);
|
||||
}
|
||||
} else if (action === 'get_openid') {
|
||||
// Handled by caller
|
||||
} else {
|
||||
console.warn('Widget postMessage event unhandled');
|
||||
this.sendError(event, {message: 'The postMessage was unhandled'});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if message origin is registered as trusted
|
||||
* @param {string} origin PostMessage origin to check
|
||||
* @return {boolean} True if trusted
|
||||
*/
|
||||
trustedEndpoint(origin) {
|
||||
if (!origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.widgetMessagingEndpoints.some((endpoint) => {
|
||||
// TODO / FIXME -- Should this also check the widgetId?
|
||||
return endpoint.endpointUrl === origin;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a postmessage response to a postMessage request
|
||||
* @param {Event} event The original postMessage request event
|
||||
* @param {Object} res Response data
|
||||
*/
|
||||
sendResponse(event, res) {
|
||||
const data = objectClone(event.data);
|
||||
data.response = res;
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an error response to a postMessage request
|
||||
* @param {Event} event The original postMessage request event
|
||||
* @param {string} msg Error message
|
||||
* @param {Error} nestedError Nested error event (optional)
|
||||
*/
|
||||
sendError(event, msg, nestedError) {
|
||||
console.error('Action:' + event.data.action + ' failed with message: ' + msg);
|
||||
const data = objectClone(event.data);
|
||||
data.response = {
|
||||
error: {
|
||||
message: msg,
|
||||
},
|
||||
};
|
||||
if (nestedError) {
|
||||
data.response.error._error = nestedError;
|
||||
}
|
||||
event.source.postMessage(data, event.origin);
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
import sanitizeHtml from 'sanitize-html';
|
||||
import { IExtendedSanitizeOptions } from './@types/sanitize-html';
|
||||
import * as linkify from 'linkifyjs';
|
||||
import linkifyMatrix from './linkify-matrix';
|
||||
import _linkifyElement from 'linkifyjs/element';
|
||||
|
@ -52,7 +53,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
|
|||
|
||||
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
|
||||
|
||||
const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
|
||||
|
||||
/*
|
||||
* Return true if the given string contains emoji
|
||||
|
@ -151,7 +152,7 @@ export function isUrlPermitted(inputUrl: string) {
|
|||
}
|
||||
}
|
||||
|
||||
const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to matrix
|
||||
const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to matrix
|
||||
// add blank targets to all hyperlinks except vector URLs
|
||||
'a': function(tagName: string, attribs: sanitizeHtml.Attributes) {
|
||||
if (attribs.href) {
|
||||
|
@ -224,7 +225,7 @@ const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to mat
|
|||
},
|
||||
};
|
||||
|
||||
const sanitizeHtmlParams: sanitizeHtml.IOptions = {
|
||||
const sanitizeHtmlParams: IExtendedSanitizeOptions = {
|
||||
allowedTags: [
|
||||
'font', // custom to matrix for IRC-style font coloring
|
||||
'del', // for markdown
|
||||
|
@ -245,13 +246,14 @@ const sanitizeHtmlParams: sanitizeHtml.IOptions = {
|
|||
selfClosing: ['img', 'br', 'hr', 'area', 'base', 'basefont', 'input', 'link', 'meta'],
|
||||
// URL schemes we permit
|
||||
allowedSchemes: PERMITTED_URL_SCHEMES,
|
||||
|
||||
allowProtocolRelative: false,
|
||||
transformTags,
|
||||
// 50 levels deep "should be enough for anyone"
|
||||
nestingLimit: 50,
|
||||
};
|
||||
|
||||
// this is the same as the above except with less rewriting
|
||||
const composerSanitizeHtmlParams: sanitizeHtml.IOptions = {
|
||||
const composerSanitizeHtmlParams: IExtendedSanitizeOptions = {
|
||||
...sanitizeHtmlParams,
|
||||
transformTags: {
|
||||
'code': transformTags['code'],
|
||||
|
@ -339,33 +341,9 @@ class HtmlHighlighter extends BaseHighlighter<string> {
|
|||
}
|
||||
}
|
||||
|
||||
class TextHighlighter extends BaseHighlighter<React.ReactNode> {
|
||||
private key = 0;
|
||||
|
||||
/* create a <span> node to hold the given content
|
||||
*
|
||||
* snippet: content of the span
|
||||
* highlight: true to highlight as a search match
|
||||
*
|
||||
* returns a React node
|
||||
*/
|
||||
protected processSnippet(snippet: string, highlight: boolean): React.ReactNode {
|
||||
const key = this.key++;
|
||||
|
||||
let node = <span key={key} className={highlight ? this.highlightClass : null}>
|
||||
{ snippet }
|
||||
</span>;
|
||||
|
||||
if (highlight && this.highlightLink) {
|
||||
node = <a key={key} href={this.highlightLink}>{ node }</a>;
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
interface IContent {
|
||||
format?: string;
|
||||
// eslint-disable-next-line camelcase
|
||||
formatted_body?: string;
|
||||
body: string;
|
||||
}
|
||||
|
@ -474,8 +452,13 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
|
|||
});
|
||||
|
||||
return isDisplayedWithHtml ?
|
||||
<span key="body" ref={opts.ref} className={className} dangerouslySetInnerHTML={{ __html: safeBody }} dir="auto" /> :
|
||||
<span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
|
||||
<span
|
||||
key="body"
|
||||
ref={opts.ref}
|
||||
className={className}
|
||||
dangerouslySetInnerHTML={{ __html: safeBody }}
|
||||
dir="auto"
|
||||
/> : <span key="body" ref={opts.ref} className={className} dir="auto">{ strippedBody }</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -17,9 +17,13 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import EventIndexPeg from './indexing/EventIndexPeg';
|
||||
import createMatrixClient from './utils/createMatrixClient';
|
||||
import Analytics from './Analytics';
|
||||
|
@ -42,48 +46,51 @@ import {Mjolnir} from "./mjolnir/Mjolnir";
|
|||
import DeviceListener from "./DeviceListener";
|
||||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
||||
interface ILoadSessionOpts {
|
||||
enableGuest?: boolean;
|
||||
guestHsUrl?: string;
|
||||
guestIsUrl?: string;
|
||||
ignoreGuest?: boolean;
|
||||
defaultDeviceDisplayName?: string;
|
||||
fragmentQueryParams?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called at startup, to attempt to build a logged-in Matrix session. It tries
|
||||
* a number of things:
|
||||
*
|
||||
*
|
||||
* 1. if we have a guest access token in the fragment query params, it uses
|
||||
* that.
|
||||
*
|
||||
* 2. if an access token is stored in local storage (from a previous session),
|
||||
* it uses that.
|
||||
*
|
||||
* 3. it attempts to auto-register as a guest user.
|
||||
*
|
||||
* If any of steps 1-4 are successful, it will call {_doSetLoggedIn}, which in
|
||||
* turn will raise on_logged_in and will_start_client events.
|
||||
*
|
||||
* @param {object} opts
|
||||
*
|
||||
* @param {object} opts.fragmentQueryParams: string->string map of the
|
||||
* @param {object} [opts]
|
||||
* @param {object} [opts.fragmentQueryParams]: string->string map of the
|
||||
* query-parameters extracted from the #-fragment of the starting URI.
|
||||
*
|
||||
* @param {boolean} opts.enableGuest: set to true to enable guest access tokens
|
||||
* and auto-guest registrations.
|
||||
*
|
||||
* @params {string} opts.guestHsUrl: homeserver URL. Only used if enableGuest is
|
||||
* true; defines the HS to register against.
|
||||
*
|
||||
* @params {string} opts.guestIsUrl: homeserver URL. Only used if enableGuest is
|
||||
* true; defines the IS to use.
|
||||
*
|
||||
* @params {bool} opts.ignoreGuest: If the stored session is a guest account, ignore
|
||||
* it and don't load it.
|
||||
*
|
||||
* @param {boolean} [opts.enableGuest]: set to true to enable guest access
|
||||
* tokens and auto-guest registrations.
|
||||
* @param {string} [opts.guestHsUrl]: homeserver URL. Only used if enableGuest
|
||||
* is true; defines the HS to register against.
|
||||
* @param {string} [opts.guestIsUrl]: homeserver URL. Only used if enableGuest
|
||||
* is true; defines the IS to use.
|
||||
* @param {bool} [opts.ignoreGuest]: If the stored session is a guest account,
|
||||
* ignore it and don't load it.
|
||||
* @param {string} [opts.defaultDeviceDisplayName]: Default display name to use
|
||||
* when registering as a guest.
|
||||
* @returns {Promise} a promise which resolves when the above process completes.
|
||||
* Resolves to `true` if we ended up starting a session, or `false` if we
|
||||
* failed.
|
||||
*/
|
||||
export async function loadSession(opts) {
|
||||
export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean> {
|
||||
try {
|
||||
let enableGuest = opts.enableGuest || false;
|
||||
const guestHsUrl = opts.guestHsUrl;
|
||||
|
@ -96,12 +103,13 @@ export async function loadSession(opts) {
|
|||
enableGuest = false;
|
||||
}
|
||||
|
||||
if (enableGuest &&
|
||||
if (
|
||||
enableGuest &&
|
||||
fragmentQueryParams.guest_user_id &&
|
||||
fragmentQueryParams.guest_access_token
|
||||
) {
|
||||
console.log("Using guest access credentials");
|
||||
return _doSetLoggedIn({
|
||||
return doSetLoggedIn({
|
||||
userId: fragmentQueryParams.guest_user_id,
|
||||
accessToken: fragmentQueryParams.guest_access_token,
|
||||
homeserverUrl: guestHsUrl,
|
||||
|
@ -109,7 +117,7 @@ export async function loadSession(opts) {
|
|||
guest: true,
|
||||
}, true).then(() => true);
|
||||
}
|
||||
const success = await _restoreFromLocalStorage({
|
||||
const success = await restoreFromLocalStorage({
|
||||
ignoreGuest: Boolean(opts.ignoreGuest),
|
||||
});
|
||||
if (success) {
|
||||
|
@ -117,7 +125,7 @@ export async function loadSession(opts) {
|
|||
}
|
||||
|
||||
if (enableGuest) {
|
||||
return _registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
|
||||
return registerAsGuest(guestHsUrl, guestIsUrl, defaultDeviceDisplayName);
|
||||
}
|
||||
|
||||
// fall back to welcome screen
|
||||
|
@ -128,7 +136,7 @@ export async function loadSession(opts) {
|
|||
// need to show the general failure dialog. Instead, just go back to welcome.
|
||||
return false;
|
||||
}
|
||||
return _handleLoadSessionFailure(e);
|
||||
return handleLoadSessionFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -138,7 +146,7 @@ export async function loadSession(opts) {
|
|||
* is associated with them. The session is not loaded.
|
||||
* @returns {String} The persisted session's owner, if an owner exists. Null otherwise.
|
||||
*/
|
||||
export function getStoredSessionOwner() {
|
||||
export function getStoredSessionOwner(): string {
|
||||
const {hsUrl, userId, accessToken} = getLocalStorageSessionVars();
|
||||
return hsUrl && userId && accessToken ? userId : null;
|
||||
}
|
||||
|
@ -147,7 +155,7 @@ export function getStoredSessionOwner() {
|
|||
* @returns {bool} True if the stored session is for a guest user or false if it is
|
||||
* for a real user. If there is no stored session, return null.
|
||||
*/
|
||||
export function getStoredSessionIsGuest() {
|
||||
export function getStoredSessionIsGuest(): boolean {
|
||||
const sessVars = getLocalStorageSessionVars();
|
||||
return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
|
||||
}
|
||||
|
@ -162,7 +170,10 @@ export function getStoredSessionIsGuest() {
|
|||
* @returns {Promise} promise which resolves to true if we completed the token
|
||||
* login, else false
|
||||
*/
|
||||
export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
||||
export function attemptTokenLogin(
|
||||
queryParams: Record<string, string>,
|
||||
defaultDeviceDisplayName?: string,
|
||||
): Promise<boolean> {
|
||||
if (!queryParams.loginToken) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
@ -183,8 +194,10 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
|||
},
|
||||
).then(function(creds) {
|
||||
console.log("Logged in with token");
|
||||
return _clearStorage().then(() => {
|
||||
_persistCredentialsToLocalStorage(creds);
|
||||
return clearStorage().then(() => {
|
||||
persistCredentialsToLocalStorage(creds);
|
||||
// remember that we just logged in
|
||||
sessionStorage.setItem("mx_fresh_login", String(true));
|
||||
return true;
|
||||
});
|
||||
}).catch((err) => {
|
||||
|
@ -194,8 +207,8 @@ export function attemptTokenLogin(queryParams, defaultDeviceDisplayName) {
|
|||
});
|
||||
}
|
||||
|
||||
export function handleInvalidStoreError(e) {
|
||||
if (e.reason === Matrix.InvalidStoreError.TOGGLED_LAZY_LOADING) {
|
||||
export function handleInvalidStoreError(e: InvalidStoreError): Promise<void> {
|
||||
if (e.reason === InvalidStoreError.TOGGLED_LAZY_LOADING) {
|
||||
return Promise.resolve().then(() => {
|
||||
const lazyLoadEnabled = e.value;
|
||||
if (lazyLoadEnabled) {
|
||||
|
@ -228,7 +241,11 @@ export function handleInvalidStoreError(e) {
|
|||
}
|
||||
}
|
||||
|
||||
function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
||||
function registerAsGuest(
|
||||
hsUrl: string,
|
||||
isUrl: string,
|
||||
defaultDeviceDisplayName: string,
|
||||
): Promise<boolean> {
|
||||
console.log(`Doing guest login on ${hsUrl}`);
|
||||
|
||||
// create a temporary MatrixClient to do the login
|
||||
|
@ -242,7 +259,7 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
|||
},
|
||||
}).then((creds) => {
|
||||
console.log(`Registered as guest: ${creds.user_id}`);
|
||||
return _doSetLoggedIn({
|
||||
return doSetLoggedIn({
|
||||
userId: creds.user_id,
|
||||
deviceId: creds.device_id,
|
||||
accessToken: creds.access_token,
|
||||
|
@ -256,12 +273,21 @@ function _registerAsGuest(hsUrl, isUrl, defaultDeviceDisplayName) {
|
|||
});
|
||||
}
|
||||
|
||||
export interface ILocalStorageSession {
|
||||
hsUrl: string;
|
||||
isUrl: string;
|
||||
accessToken: string;
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
isGuest: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves information about the stored session in localstorage. The session
|
||||
* may not be valid, as it is not tested for consistency here.
|
||||
* @returns {Object} Information about the session - see implementation for variables.
|
||||
*/
|
||||
export function getLocalStorageSessionVars() {
|
||||
export function getLocalStorageSessionVars(): ILocalStorageSession {
|
||||
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
|
||||
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
|
||||
const accessToken = localStorage.getItem("mx_access_token");
|
||||
|
@ -289,8 +315,8 @@ export function getLocalStorageSessionVars() {
|
|||
// The plan is to gradually move the localStorage access done here into
|
||||
// SessionStore to avoid bugs where the view becomes out-of-sync with
|
||||
// localStorage (e.g. isGuest etc.)
|
||||
async function _restoreFromLocalStorage(opts) {
|
||||
const ignoreGuest = opts.ignoreGuest;
|
||||
async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
|
||||
const ignoreGuest = opts?.ignoreGuest;
|
||||
|
||||
if (!localStorage) {
|
||||
return false;
|
||||
|
@ -311,8 +337,11 @@ async function _restoreFromLocalStorage(opts) {
|
|||
console.log("No pickle key available");
|
||||
}
|
||||
|
||||
const freshLogin = sessionStorage.getItem("mx_fresh_login") === "true";
|
||||
sessionStorage.removeItem("mx_fresh_login");
|
||||
|
||||
console.log(`Restoring session for ${userId}`);
|
||||
await _doSetLoggedIn({
|
||||
await doSetLoggedIn({
|
||||
userId: userId,
|
||||
deviceId: deviceId,
|
||||
accessToken: accessToken,
|
||||
|
@ -320,6 +349,7 @@ async function _restoreFromLocalStorage(opts) {
|
|||
identityServerUrl: isUrl,
|
||||
guest: isGuest,
|
||||
pickleKey: pickleKey,
|
||||
freshLogin: freshLogin,
|
||||
}, false);
|
||||
return true;
|
||||
} else {
|
||||
|
@ -328,7 +358,7 @@ async function _restoreFromLocalStorage(opts) {
|
|||
}
|
||||
}
|
||||
|
||||
async function _handleLoadSessionFailure(e) {
|
||||
async function handleLoadSessionFailure(e: Error): Promise<boolean> {
|
||||
console.error("Unable to load session", e);
|
||||
|
||||
const SessionRestoreErrorDialog =
|
||||
|
@ -341,7 +371,7 @@ async function _handleLoadSessionFailure(e) {
|
|||
const [success] = await modal.finished;
|
||||
if (success) {
|
||||
// user clicked continue.
|
||||
await _clearStorage();
|
||||
await clearStorage();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -362,7 +392,8 @@ async function _handleLoadSessionFailure(e) {
|
|||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export async function setLoggedIn(credentials) {
|
||||
export async function setLoggedIn(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||
credentials.freshLogin = true;
|
||||
stopMatrixClient();
|
||||
const pickleKey = credentials.userId && credentials.deviceId
|
||||
? await PlatformPeg.get().createPickleKey(credentials.userId, credentials.deviceId)
|
||||
|
@ -374,7 +405,7 @@ export async function setLoggedIn(credentials) {
|
|||
console.log("Pickle key not created");
|
||||
}
|
||||
|
||||
return _doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
||||
return doSetLoggedIn(Object.assign({}, credentials, {pickleKey}), true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -392,7 +423,7 @@ export async function setLoggedIn(credentials) {
|
|||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
export function hydrateSession(credentials) {
|
||||
export function hydrateSession(credentials: IMatrixClientCreds): Promise<MatrixClient> {
|
||||
const oldUserId = MatrixClientPeg.get().getUserId();
|
||||
const oldDeviceId = MatrixClientPeg.get().getDeviceId();
|
||||
|
||||
|
@ -405,7 +436,7 @@ export function hydrateSession(credentials) {
|
|||
console.warn("Clearing all data: Old session belongs to a different user/session");
|
||||
}
|
||||
|
||||
return _doSetLoggedIn(credentials, overwrite);
|
||||
return doSetLoggedIn(credentials, overwrite);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -417,7 +448,10 @@ export function hydrateSession(credentials) {
|
|||
*
|
||||
* @returns {Promise} promise which resolves to the new MatrixClient once it has been started
|
||||
*/
|
||||
async function _doSetLoggedIn(credentials, clearStorage) {
|
||||
async function doSetLoggedIn(
|
||||
credentials: IMatrixClientCreds,
|
||||
clearStorageEnabled: boolean,
|
||||
): Promise<MatrixClient> {
|
||||
credentials.guest = Boolean(credentials.guest);
|
||||
|
||||
const softLogout = isSoftLogout();
|
||||
|
@ -428,6 +462,7 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
" guest: " + credentials.guest +
|
||||
" hs: " + credentials.homeserverUrl +
|
||||
" softLogout: " + softLogout,
|
||||
" freshLogin: " + credentials.freshLogin,
|
||||
);
|
||||
|
||||
// This is dispatched to indicate that the user is still in the process of logging in
|
||||
|
@ -439,8 +474,8 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
// (dis.dispatch uses `setTimeout`, which does not guarantee ordering.)
|
||||
dis.dispatch({action: 'on_logging_in'}, true);
|
||||
|
||||
if (clearStorage) {
|
||||
await _clearStorage();
|
||||
if (clearStorageEnabled) {
|
||||
await clearStorage();
|
||||
}
|
||||
|
||||
const results = await StorageManager.checkConsistency();
|
||||
|
@ -448,9 +483,9 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
// crypto store, we'll be generally confused when handling encrypted data.
|
||||
// Show a modal recommending a full reset of storage.
|
||||
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
|
||||
const signOut = await _showStorageEvictedDialog();
|
||||
const signOut = await showStorageEvictedDialog();
|
||||
if (signOut) {
|
||||
await _clearStorage();
|
||||
await clearStorage();
|
||||
// This error feels a bit clunky, but we want to make sure we don't go any
|
||||
// further and instead head back to sign in.
|
||||
throw new AbortLoginAndRebuildStorage(
|
||||
|
@ -461,19 +496,26 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
|
||||
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
|
||||
|
||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||
const client = MatrixClientPeg.get();
|
||||
|
||||
if (credentials.freshLogin && SettingsStore.getValue("feature_dehydration")) {
|
||||
// If we just logged in, try to rehydrate a device instead of using a
|
||||
// new device. If it succeeds, we'll get a new device ID, so make sure
|
||||
// we persist that ID to localStorage
|
||||
const newDeviceId = await client.rehydrateDevice();
|
||||
if (newDeviceId) {
|
||||
credentials.deviceId = newDeviceId;
|
||||
}
|
||||
|
||||
delete credentials.freshLogin;
|
||||
}
|
||||
|
||||
if (localStorage) {
|
||||
try {
|
||||
_persistCredentialsToLocalStorage(credentials);
|
||||
|
||||
// The user registered as a PWLU (PassWord-Less User), the generated password
|
||||
// is cached here such that the user can change it at a later time.
|
||||
if (credentials.password) {
|
||||
// Update SessionStore
|
||||
dis.dispatch({
|
||||
action: 'cached_password',
|
||||
cachedPassword: credentials.password,
|
||||
});
|
||||
}
|
||||
persistCredentialsToLocalStorage(credentials);
|
||||
// make sure we don't think that it's a fresh login any more
|
||||
sessionStorage.removeItem("mx_fresh_login");
|
||||
} catch (e) {
|
||||
console.warn("Error using local storage: can't persist session!", e);
|
||||
}
|
||||
|
@ -481,15 +523,13 @@ async function _doSetLoggedIn(credentials, clearStorage) {
|
|||
console.warn("No local storage available: can't persist session!");
|
||||
}
|
||||
|
||||
MatrixClientPeg.replaceUsingCreds(credentials);
|
||||
|
||||
dis.dispatch({ action: 'on_logged_in' });
|
||||
|
||||
await startMatrixClient(/*startSyncing=*/!softLogout);
|
||||
return MatrixClientPeg.get();
|
||||
return client;
|
||||
}
|
||||
|
||||
function _showStorageEvictedDialog() {
|
||||
function showStorageEvictedDialog(): Promise<boolean> {
|
||||
const StorageEvictedDialog = sdk.getComponent('views.dialogs.StorageEvictedDialog');
|
||||
return new Promise(resolve => {
|
||||
Modal.createTrackedDialog('Storage evicted', '', StorageEvictedDialog, {
|
||||
|
@ -502,7 +542,7 @@ function _showStorageEvictedDialog() {
|
|||
// `instanceof`. Babel 7 supports this natively in their class handling.
|
||||
class AbortLoginAndRebuildStorage extends Error { }
|
||||
|
||||
function _persistCredentialsToLocalStorage(credentials) {
|
||||
function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void {
|
||||
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
|
||||
if (credentials.identityServerUrl) {
|
||||
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
|
||||
|
@ -512,7 +552,7 @@ function _persistCredentialsToLocalStorage(credentials) {
|
|||
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
|
||||
|
||||
if (credentials.pickleKey) {
|
||||
localStorage.setItem("mx_has_pickle_key", true);
|
||||
localStorage.setItem("mx_has_pickle_key", String(true));
|
||||
} else {
|
||||
if (localStorage.getItem("mx_has_pickle_key")) {
|
||||
console.error("Expected a pickle key, but none provided. Encryption may not work.");
|
||||
|
@ -528,6 +568,8 @@ function _persistCredentialsToLocalStorage(credentials) {
|
|||
localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||
}
|
||||
|
||||
SecurityCustomisations.persistCredentials?.(credentials);
|
||||
|
||||
console.log(`Session persisted for ${credentials.userId}`);
|
||||
}
|
||||
|
||||
|
@ -536,7 +578,7 @@ let _isLoggingOut = false;
|
|||
/**
|
||||
* Logs the current session out and transitions to the logged-out state
|
||||
*/
|
||||
export function logout() {
|
||||
export function logout(): void {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
if (MatrixClientPeg.get().isGuest()) {
|
||||
|
@ -565,7 +607,7 @@ export function logout() {
|
|||
);
|
||||
}
|
||||
|
||||
export function softLogout() {
|
||||
export function softLogout(): void {
|
||||
if (!MatrixClientPeg.get()) return;
|
||||
|
||||
// Track that we've detected and trapped a soft logout. This helps prevent other
|
||||
|
@ -586,11 +628,11 @@ export function softLogout() {
|
|||
// DO NOT CALL LOGOUT. A soft logout preserves data, logout does not.
|
||||
}
|
||||
|
||||
export function isSoftLogout() {
|
||||
export function isSoftLogout(): boolean {
|
||||
return localStorage.getItem("mx_soft_logout") === "true";
|
||||
}
|
||||
|
||||
export function isLoggingOut() {
|
||||
export function isLoggingOut(): boolean {
|
||||
return _isLoggingOut;
|
||||
}
|
||||
|
||||
|
@ -600,7 +642,7 @@ export function isLoggingOut() {
|
|||
* @param {boolean} startSyncing True (default) to actually start
|
||||
* syncing the client.
|
||||
*/
|
||||
async function startMatrixClient(startSyncing=true) {
|
||||
async function startMatrixClient(startSyncing = true): Promise<void> {
|
||||
console.log(`Lifecycle: Starting MatrixClient`);
|
||||
|
||||
// dispatch this before starting the matrix client: it's used
|
||||
|
@ -659,24 +701,37 @@ async function startMatrixClient(startSyncing=true) {
|
|||
* Stops a running client and all related services, and clears persistent
|
||||
* storage. Used after a session has been logged out.
|
||||
*/
|
||||
export async function onLoggedOut() {
|
||||
export async function onLoggedOut(): Promise<void> {
|
||||
_isLoggingOut = false;
|
||||
// Ensure that we dispatch a view change **before** stopping the client so
|
||||
// so that React components unmount first. This avoids React soft crashes
|
||||
// that can occur when components try to use a null client.
|
||||
dis.dispatch({action: 'on_logged_out'}, true);
|
||||
stopMatrixClient();
|
||||
await _clearStorage();
|
||||
await clearStorage({deleteEverything: true});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {object} opts Options for how to clear storage.
|
||||
* @returns {Promise} promise which resolves once the stores have been cleared
|
||||
*/
|
||||
async function _clearStorage() {
|
||||
async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void> {
|
||||
Analytics.disable();
|
||||
|
||||
if (window.localStorage) {
|
||||
// try to save any 3pid invites from being obliterated
|
||||
const pendingInvites = ThreepidInviteStore.instance.getWireInvites();
|
||||
|
||||
window.localStorage.clear();
|
||||
|
||||
// now restore those invites
|
||||
if (!opts?.deleteEverything) {
|
||||
pendingInvites.forEach(i => {
|
||||
const roomId = i.roomId;
|
||||
delete i.roomId; // delete to avoid confusing the store
|
||||
ThreepidInviteStore.instance.storeInvite(roomId, i);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (window.sessionStorage) {
|
||||
|
@ -698,7 +753,7 @@ async function _clearStorage() {
|
|||
* @param {boolean} unsetClient True (default) to abandon the client
|
||||
* on MatrixClientPeg after stopping.
|
||||
*/
|
||||
export function stopMatrixClient(unsetClient=true) {
|
||||
export function stopMatrixClient(unsetClient = true): void {
|
||||
Notifier.stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
|
@ -18,35 +18,73 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from "matrix-js-sdk";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
||||
interface ILoginOptions {
|
||||
defaultDeviceDisplayName?: string;
|
||||
}
|
||||
|
||||
// TODO: Move this to JS SDK
|
||||
interface ILoginFlow {
|
||||
type: string;
|
||||
}
|
||||
|
||||
// TODO: Move this to JS SDK
|
||||
/* eslint-disable camelcase */
|
||||
interface ILoginParams {
|
||||
identifier?: string;
|
||||
password?: string;
|
||||
token?: string;
|
||||
device_id?: string;
|
||||
initial_device_display_name?: string;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
export default class Login {
|
||||
constructor(hsUrl, isUrl, fallbackHsUrl, opts) {
|
||||
this._hsUrl = hsUrl;
|
||||
this._isUrl = isUrl;
|
||||
this._fallbackHsUrl = fallbackHsUrl;
|
||||
this._currentFlowIndex = 0;
|
||||
this._flows = [];
|
||||
this._defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this._tempClient = null; // memoize
|
||||
private hsUrl: string;
|
||||
private isUrl: string;
|
||||
private fallbackHsUrl: string;
|
||||
private currentFlowIndex: number;
|
||||
// TODO: Flows need a type in JS SDK
|
||||
private flows: Array<ILoginFlow>;
|
||||
private defaultDeviceDisplayName: string;
|
||||
private tempClient: MatrixClient;
|
||||
|
||||
constructor(
|
||||
hsUrl: string,
|
||||
isUrl: string,
|
||||
fallbackHsUrl?: string,
|
||||
opts?: ILoginOptions,
|
||||
) {
|
||||
this.hsUrl = hsUrl;
|
||||
this.isUrl = isUrl;
|
||||
this.fallbackHsUrl = fallbackHsUrl;
|
||||
this.currentFlowIndex = 0;
|
||||
this.flows = [];
|
||||
this.defaultDeviceDisplayName = opts.defaultDeviceDisplayName;
|
||||
this.tempClient = null; // memoize
|
||||
}
|
||||
|
||||
getHomeserverUrl() {
|
||||
return this._hsUrl;
|
||||
public getHomeserverUrl(): string {
|
||||
return this.hsUrl;
|
||||
}
|
||||
|
||||
getIdentityServerUrl() {
|
||||
return this._isUrl;
|
||||
public getIdentityServerUrl(): string {
|
||||
return this.isUrl;
|
||||
}
|
||||
|
||||
setHomeserverUrl(hsUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._hsUrl = hsUrl;
|
||||
public setHomeserverUrl(hsUrl: string): void {
|
||||
this.tempClient = null; // clear memoization
|
||||
this.hsUrl = hsUrl;
|
||||
}
|
||||
|
||||
setIdentityServerUrl(isUrl) {
|
||||
this._tempClient = null; // clear memoization
|
||||
this._isUrl = isUrl;
|
||||
public setIdentityServerUrl(isUrl: string): void {
|
||||
this.tempClient = null; // clear memoization
|
||||
this.isUrl = isUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,40 +92,41 @@ export default class Login {
|
|||
* requests.
|
||||
* @returns {MatrixClient}
|
||||
*/
|
||||
createTemporaryClient() {
|
||||
if (this._tempClient) return this._tempClient; // use memoization
|
||||
return this._tempClient = Matrix.createClient({
|
||||
baseUrl: this._hsUrl,
|
||||
idBaseUrl: this._isUrl,
|
||||
public createTemporaryClient(): MatrixClient {
|
||||
if (this.tempClient) return this.tempClient; // use memoization
|
||||
return this.tempClient = Matrix.createClient({
|
||||
baseUrl: this.hsUrl,
|
||||
idBaseUrl: this.isUrl,
|
||||
});
|
||||
}
|
||||
|
||||
getFlows() {
|
||||
const self = this;
|
||||
public async getFlows(): Promise<Array<ILoginFlow>> {
|
||||
const client = this.createTemporaryClient();
|
||||
return client.loginFlows().then(function(result) {
|
||||
self._flows = result.flows;
|
||||
self._currentFlowIndex = 0;
|
||||
const { flows } = await client.loginFlows();
|
||||
this.flows = flows;
|
||||
this.currentFlowIndex = 0;
|
||||
// technically the UI should display options for all flows for the
|
||||
// user to then choose one, so return all the flows here.
|
||||
return self._flows;
|
||||
});
|
||||
return this.flows;
|
||||
}
|
||||
|
||||
chooseFlow(flowIndex) {
|
||||
this._currentFlowIndex = flowIndex;
|
||||
public chooseFlow(flowIndex): void {
|
||||
this.currentFlowIndex = flowIndex;
|
||||
}
|
||||
|
||||
getCurrentFlowStep() {
|
||||
public getCurrentFlowStep(): string {
|
||||
// technically the flow can have multiple steps, but no one does this
|
||||
// for login so we can ignore it.
|
||||
const flowStep = this._flows[this._currentFlowIndex];
|
||||
const flowStep = this.flows[this.currentFlowIndex];
|
||||
return flowStep ? flowStep.type : null;
|
||||
}
|
||||
|
||||
loginViaPassword(username, phoneCountry, phoneNumber, pass) {
|
||||
const self = this;
|
||||
|
||||
public loginViaPassword(
|
||||
username: string,
|
||||
phoneCountry: string,
|
||||
phoneNumber: string,
|
||||
password: string,
|
||||
): Promise<IMatrixClientCreds> {
|
||||
const isEmail = username.indexOf("@") > 0;
|
||||
|
||||
let identifier;
|
||||
|
@ -113,14 +152,14 @@ export default class Login {
|
|||
}
|
||||
|
||||
const loginParams = {
|
||||
password: pass,
|
||||
identifier: identifier,
|
||||
initial_device_display_name: this._defaultDeviceDisplayName,
|
||||
password,
|
||||
identifier,
|
||||
initial_device_display_name: this.defaultDeviceDisplayName,
|
||||
};
|
||||
|
||||
const tryFallbackHs = (originalError) => {
|
||||
return sendLoginRequest(
|
||||
self._fallbackHsUrl, this._isUrl, 'm.login.password', loginParams,
|
||||
this.fallbackHsUrl, this.isUrl, 'm.login.password', loginParams,
|
||||
).catch((fallbackError) => {
|
||||
console.log("fallback HS login failed", fallbackError);
|
||||
// throw the original error
|
||||
|
@ -130,11 +169,11 @@ export default class Login {
|
|||
|
||||
let originalLoginError = null;
|
||||
return sendLoginRequest(
|
||||
self._hsUrl, self._isUrl, 'm.login.password', loginParams,
|
||||
this.hsUrl, this.isUrl, 'm.login.password', loginParams,
|
||||
).catch((error) => {
|
||||
originalLoginError = error;
|
||||
if (error.httpStatus === 403) {
|
||||
if (self._fallbackHsUrl) {
|
||||
if (this.fallbackHsUrl) {
|
||||
return tryFallbackHs(originalLoginError);
|
||||
}
|
||||
}
|
||||
|
@ -154,11 +193,16 @@ export default class Login {
|
|||
* @param {string} hsUrl the base url of the Homeserver used to log in.
|
||||
* @param {string} isUrl the base url of the default identity server
|
||||
* @param {string} loginType the type of login to do
|
||||
* @param {object} loginParams the parameters for the login
|
||||
* @param {ILoginParams} loginParams the parameters for the login
|
||||
*
|
||||
* @returns {MatrixClientCreds}
|
||||
*/
|
||||
export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
|
||||
export async function sendLoginRequest(
|
||||
hsUrl: string,
|
||||
isUrl: string,
|
||||
loginType: string,
|
||||
loginParams: ILoginParams,
|
||||
): Promise<IMatrixClientCreds> {
|
||||
const client = Matrix.createClient({
|
||||
baseUrl: hsUrl,
|
||||
idBaseUrl: isUrl,
|
||||
|
@ -179,11 +223,15 @@ export async function sendLoginRequest(hsUrl, isUrl, loginType, loginParams) {
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const creds: IMatrixClientCreds = {
|
||||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
};
|
||||
|
||||
SecurityCustomisations.examineLoginResponse?.(data, creds);
|
||||
|
||||
return creds;
|
||||
}
|