Merge branch 'develop' into feature-multi-language-spell-check

This commit is contained in:
Šimon Brandner 2021-02-18 18:16:16 +01:00
commit bd0e5446c5
No known key found for this signature in database
GPG key ID: 9760693FDD98A790
296 changed files with 21708 additions and 6831 deletions

View file

@ -1,4 +1,4 @@
src/component-index.js src/component-index.js
test/end-to-end-tests/node_modules/ test/end-to-end-tests/node_modules/
test/end-to-end-tests/riot/ test/end-to-end-tests/element/
test/end-to-end-tests/synapse/ test/end-to-end-tests/synapse/

View file

@ -12,5 +12,5 @@ test/components/views/dialogs/InteractiveAuthDialog-test.js
test/mock-clock.js test/mock-clock.js
src/component-index.js src/component-index.js
test/end-to-end-tests/node_modules/ test/end-to-end-tests/node_modules/
test/end-to-end-tests/riot/ test/end-to-end-tests/element/
test/end-to-end-tests/synapse/ test/end-to-end-tests/synapse/

View file

@ -22,6 +22,8 @@ module.exports = {
"files": ["src/**/*.{ts,tsx}"], "files": ["src/**/*.{ts,tsx}"],
"extends": ["matrix-org/ts"], "extends": ["matrix-org/ts"],
"rules": { "rules": {
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning // We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do // We'd rather not do this but we do

View file

@ -1,3 +1,467 @@
Changes in [3.14.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0) (2021-02-16)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.14.0-rc.1...v3.14.0)
* Upgrade to JS SDK 9.7.0
* [Release] Use config for host signup branding
[\#5651](https://github.com/matrix-org/matrix-react-sdk/pull/5651)
Changes in [3.14.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.14.0-rc.1) (2021-02-10)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.1...v3.14.0-rc.1)
* Upgrade to JS SDK 9.7.0-rc.1
* Translations update from Weblate
[\#5636](https://github.com/matrix-org/matrix-react-sdk/pull/5636)
* Add host signup modal with iframe
[\#5450](https://github.com/matrix-org/matrix-react-sdk/pull/5450)
* Fix duplication of codeblock elements
[\#5633](https://github.com/matrix-org/matrix-react-sdk/pull/5633)
* Handle undefined call stats
[\#5632](https://github.com/matrix-org/matrix-react-sdk/pull/5632)
* Avoid delayed displaying of sources in source picker
[\#5631](https://github.com/matrix-org/matrix-react-sdk/pull/5631)
* Give breadcrumbs toolbar an accessibility label.
[\#5628](https://github.com/matrix-org/matrix-react-sdk/pull/5628)
* Fix the %s in logs
[\#5627](https://github.com/matrix-org/matrix-react-sdk/pull/5627)
* Fix jumpy notifications settings UI
[\#5625](https://github.com/matrix-org/matrix-react-sdk/pull/5625)
* Improve displaying of code blocks
[\#5559](https://github.com/matrix-org/matrix-react-sdk/pull/5559)
* Fix desktop Matrix screen sharing and add a screen/window picker
[\#5525](https://github.com/matrix-org/matrix-react-sdk/pull/5525)
* Call "MatrixClientPeg.get()" only once in method "findOverrideMuteRule"
[\#5498](https://github.com/matrix-org/matrix-react-sdk/pull/5498)
* Close current modal when session is logged out
[\#5616](https://github.com/matrix-org/matrix-react-sdk/pull/5616)
* Switch room explorer list to CSS grid
[\#5551](https://github.com/matrix-org/matrix-react-sdk/pull/5551)
* Improve SSO login start screen and 3pid invite handling somewhat
[\#5622](https://github.com/matrix-org/matrix-react-sdk/pull/5622)
* Don't jump to bottom on reaction
[\#5621](https://github.com/matrix-org/matrix-react-sdk/pull/5621)
* Fix several profile settings oddities
[\#5620](https://github.com/matrix-org/matrix-react-sdk/pull/5620)
* Add option to hide the stickers button in the composer
[\#5530](https://github.com/matrix-org/matrix-react-sdk/pull/5530)
* Fix confusing right panel button behaviour
[\#5598](https://github.com/matrix-org/matrix-react-sdk/pull/5598)
* Fix jumping timestamp if hovering a message with e2e indicator bar
[\#5601](https://github.com/matrix-org/matrix-react-sdk/pull/5601)
* Fix avatar and trash alignment
[\#5614](https://github.com/matrix-org/matrix-react-sdk/pull/5614)
* Fix z-index of stickerpicker
[\#5617](https://github.com/matrix-org/matrix-react-sdk/pull/5617)
* Fix permalink via parsing for rooms
[\#5615](https://github.com/matrix-org/matrix-react-sdk/pull/5615)
* Fix "Terms and Conditions" checkbox alignment
[\#5613](https://github.com/matrix-org/matrix-react-sdk/pull/5613)
* Fix flair height after accent changes
[\#5611](https://github.com/matrix-org/matrix-react-sdk/pull/5611)
* Iterate Social Logins work around edge cases and branding
[\#5609](https://github.com/matrix-org/matrix-react-sdk/pull/5609)
* Lock widget room ID when added
[\#5607](https://github.com/matrix-org/matrix-react-sdk/pull/5607)
* Better errors for SSO failures
[\#5605](https://github.com/matrix-org/matrix-react-sdk/pull/5605)
* Increase language search bar width
[\#5549](https://github.com/matrix-org/matrix-react-sdk/pull/5549)
* Scroll to bottom on message_sent
[\#5565](https://github.com/matrix-org/matrix-react-sdk/pull/5565)
* Fix new rooms being titled 'Empty Room'
[\#5587](https://github.com/matrix-org/matrix-react-sdk/pull/5587)
* Fix saving the collapsed state of the left panel
[\#5593](https://github.com/matrix-org/matrix-react-sdk/pull/5593)
* Fix app-url hint in the e2e-test run script output
[\#5600](https://github.com/matrix-org/matrix-react-sdk/pull/5600)
* Fix RoomView re-mounting breaking peeking
[\#5602](https://github.com/matrix-org/matrix-react-sdk/pull/5602)
* Tweak a few room ID checks
[\#5592](https://github.com/matrix-org/matrix-react-sdk/pull/5592)
* Remove pills from event permalinks with text
[\#5575](https://github.com/matrix-org/matrix-react-sdk/pull/5575)
Changes in [3.13.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.1) (2021-02-04)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0...v3.13.1)
* [Release] Fix z-index of stickerpicker
[\#5618](https://github.com/matrix-org/matrix-react-sdk/pull/5618)
Changes in [3.13.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0) (2021-02-03)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.13.0-rc.1...v3.13.0)
* Upgrade to JS SDK 9.6.0
* [Release] Fix flair height after accent changes
[\#5612](https://github.com/matrix-org/matrix-react-sdk/pull/5612)
* [Release] Iterate Social Logins work around edge cases and branding
[\#5610](https://github.com/matrix-org/matrix-react-sdk/pull/5610)
* [Release] Lock widget room ID when added
[\#5608](https://github.com/matrix-org/matrix-react-sdk/pull/5608)
* [Release] Better errors for SSO failures
[\#5606](https://github.com/matrix-org/matrix-react-sdk/pull/5606)
* [Release] Fix RoomView re-mounting breaking peeking
[\#5603](https://github.com/matrix-org/matrix-react-sdk/pull/5603)
Changes in [3.13.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.13.0-rc.1) (2021-01-29)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.1...v3.13.0-rc.1)
* Upgrade to JS SDK 9.6.0-rc.1
* Translations update from Weblate
[\#5597](https://github.com/matrix-org/matrix-react-sdk/pull/5597)
* Support managed hybrid widgets from config
[\#5596](https://github.com/matrix-org/matrix-react-sdk/pull/5596)
* Add managed hybrid call widgets when supported
[\#5594](https://github.com/matrix-org/matrix-react-sdk/pull/5594)
* Tweak mobile guide toast copy
[\#5595](https://github.com/matrix-org/matrix-react-sdk/pull/5595)
* Improve SSO auth flow
[\#5578](https://github.com/matrix-org/matrix-react-sdk/pull/5578)
* Add optional mobile guide toast
[\#5586](https://github.com/matrix-org/matrix-react-sdk/pull/5586)
* Fix invisible text after logging out in the dark theme
[\#5588](https://github.com/matrix-org/matrix-react-sdk/pull/5588)
* Fix escape for cancelling replies
[\#5591](https://github.com/matrix-org/matrix-react-sdk/pull/5591)
* Update widget-api to beta.12
[\#5589](https://github.com/matrix-org/matrix-react-sdk/pull/5589)
* Add commands for DM conversion
[\#5540](https://github.com/matrix-org/matrix-react-sdk/pull/5540)
* Run a UI refresh over the OIDC Exchange confirmation dialog
[\#5580](https://github.com/matrix-org/matrix-react-sdk/pull/5580)
* Allow stickerpickers the legacy "visibility" capability
[\#5581](https://github.com/matrix-org/matrix-react-sdk/pull/5581)
* Hide local video if it is muted
[\#5529](https://github.com/matrix-org/matrix-react-sdk/pull/5529)
* Don't use name width in reply thread for IRC layout
[\#5518](https://github.com/matrix-org/matrix-react-sdk/pull/5518)
* Update code_style.md
[\#5554](https://github.com/matrix-org/matrix-react-sdk/pull/5554)
* Fix Czech capital letters like ŠČŘ...
[\#5569](https://github.com/matrix-org/matrix-react-sdk/pull/5569)
* Add optional search shortcut
[\#5548](https://github.com/matrix-org/matrix-react-sdk/pull/5548)
* Fix Sudden 'find a room' UI shows up when the only room moves to favourites
[\#5584](https://github.com/matrix-org/matrix-react-sdk/pull/5584)
* Increase PersistedElement's z-index
[\#5568](https://github.com/matrix-org/matrix-react-sdk/pull/5568)
* Remove check that prevents Jitsi widgets from being unpinned
[\#5582](https://github.com/matrix-org/matrix-react-sdk/pull/5582)
* Fix Jitsi widgets causing localized tile crashes
[\#5583](https://github.com/matrix-org/matrix-react-sdk/pull/5583)
* Log candidates for calls
[\#5573](https://github.com/matrix-org/matrix-react-sdk/pull/5573)
* Upgrade deps 2021-01
[\#5579](https://github.com/matrix-org/matrix-react-sdk/pull/5579)
* Fix "Continuing without email" dialog bug
[\#5566](https://github.com/matrix-org/matrix-react-sdk/pull/5566)
* Require registration for verification actions
[\#5574](https://github.com/matrix-org/matrix-react-sdk/pull/5574)
* Don't play the hangup sound when the call is answered from elsewhere
[\#5572](https://github.com/matrix-org/matrix-react-sdk/pull/5572)
* Move to newer base image for end-to-end tests
[\#5570](https://github.com/matrix-org/matrix-react-sdk/pull/5570)
* Update widgets in the room upon join
[\#5564](https://github.com/matrix-org/matrix-react-sdk/pull/5564)
* Update AuxPanel and related buttons when widgets change or on reload
[\#5563](https://github.com/matrix-org/matrix-react-sdk/pull/5563)
* Add VoIP user mapper
[\#5560](https://github.com/matrix-org/matrix-react-sdk/pull/5560)
* Improve styling of SSO Buttons for multiple IdPs
[\#5558](https://github.com/matrix-org/matrix-react-sdk/pull/5558)
* Fixes for the general tab in the room dialog
[\#5522](https://github.com/matrix-org/matrix-react-sdk/pull/5522)
* fix issue 16226 to allow switching back to default HS.
[\#5561](https://github.com/matrix-org/matrix-react-sdk/pull/5561)
* Support room-defined widget layouts
[\#5553](https://github.com/matrix-org/matrix-react-sdk/pull/5553)
* Change a bunch of strings from Recovery Key/Phrase to Security Key/Phrase
[\#5533](https://github.com/matrix-org/matrix-react-sdk/pull/5533)
* Give a bigger target area to AppsDrawer vertical resizer
[\#5557](https://github.com/matrix-org/matrix-react-sdk/pull/5557)
* Fix minimized left panel avatar alignment
[\#5493](https://github.com/matrix-org/matrix-react-sdk/pull/5493)
* Ensure component index has been written before renaming
[\#5556](https://github.com/matrix-org/matrix-react-sdk/pull/5556)
* Fixed continue button while selecting home-server
[\#5552](https://github.com/matrix-org/matrix-react-sdk/pull/5552)
* Wire up MSC2931 widget navigation
[\#5527](https://github.com/matrix-org/matrix-react-sdk/pull/5527)
* Various fixes for Bridge Info page (MSC2346)
[\#5454](https://github.com/matrix-org/matrix-react-sdk/pull/5454)
* Use room-specific listeners for message preview and community prototype
[\#5547](https://github.com/matrix-org/matrix-react-sdk/pull/5547)
* Fix some misc. React warnings when viewing timeline
[\#5546](https://github.com/matrix-org/matrix-react-sdk/pull/5546)
* Use device storage for allowed widgets if account data not supported
[\#5544](https://github.com/matrix-org/matrix-react-sdk/pull/5544)
* Fix incoming call box on dark theme
[\#5542](https://github.com/matrix-org/matrix-react-sdk/pull/5542)
* Convert DMRoomMap to typescript
[\#5541](https://github.com/matrix-org/matrix-react-sdk/pull/5541)
* Add in-call dialpad for DTMF sending
[\#5532](https://github.com/matrix-org/matrix-react-sdk/pull/5532)
Changes in [3.12.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.1) (2021-01-26)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0...v3.12.1)
* Upgrade to JS SDK 9.5.1
Changes in [3.12.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0) (2021-01-18)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.12.0-rc.1...v3.12.0)
* Upgrade to JS SDK 9.5.0
* Fix incoming call box on dark theme
[\#5543](https://github.com/matrix-org/matrix-react-sdk/pull/5543)
Changes in [3.12.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.12.0-rc.1) (2021-01-13)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.1...v3.12.0-rc.1)
* Upgrade to JS SDK 9.5.0-rc.1
* Fix soft crash on soft logout page
[\#5539](https://github.com/matrix-org/matrix-react-sdk/pull/5539)
* Translations update from Weblate
[\#5538](https://github.com/matrix-org/matrix-react-sdk/pull/5538)
* Run TypeScript tests
[\#5537](https://github.com/matrix-org/matrix-react-sdk/pull/5537)
* Add a basic widget explorer to devtools (per-room)
[\#5528](https://github.com/matrix-org/matrix-react-sdk/pull/5528)
* Add <input type="password"> to security key field
[\#5534](https://github.com/matrix-org/matrix-react-sdk/pull/5534)
* Fix avatar upload prompt/tooltip floating wrong and permissions
[\#5526](https://github.com/matrix-org/matrix-react-sdk/pull/5526)
* Add a dialpad UI for PSTN lookup
[\#5523](https://github.com/matrix-org/matrix-react-sdk/pull/5523)
* Basic call transfer initiation support
[\#5494](https://github.com/matrix-org/matrix-react-sdk/pull/5494)
* Fix #15988
[\#5524](https://github.com/matrix-org/matrix-react-sdk/pull/5524)
* Bump node-notifier from 8.0.0 to 8.0.1
[\#5520](https://github.com/matrix-org/matrix-react-sdk/pull/5520)
* Use TypeScript source for development, swap to build during release
[\#5503](https://github.com/matrix-org/matrix-react-sdk/pull/5503)
* Look for emoji in the body that will be displayed
[\#5517](https://github.com/matrix-org/matrix-react-sdk/pull/5517)
* Bump ini from 1.3.5 to 1.3.7
[\#5486](https://github.com/matrix-org/matrix-react-sdk/pull/5486)
* Recognise `*.element.io` links as Element permalinks
[\#5514](https://github.com/matrix-org/matrix-react-sdk/pull/5514)
* Fixes for call UI
[\#5509](https://github.com/matrix-org/matrix-react-sdk/pull/5509)
* Add a snowfall chat effect (with /snowfall command)
[\#5511](https://github.com/matrix-org/matrix-react-sdk/pull/5511)
* fireworks effect
[\#5507](https://github.com/matrix-org/matrix-react-sdk/pull/5507)
* Don't play call end sound for calls that never started
[\#5506](https://github.com/matrix-org/matrix-react-sdk/pull/5506)
* Add /tableflip slash command
[\#5485](https://github.com/matrix-org/matrix-react-sdk/pull/5485)
* Import from src in IncomingCallBox.tsx
[\#5504](https://github.com/matrix-org/matrix-react-sdk/pull/5504)
* Social Login support both https and mxc icons
[\#5499](https://github.com/matrix-org/matrix-react-sdk/pull/5499)
* Fix padding in confirmation email registration prompt
[\#5501](https://github.com/matrix-org/matrix-react-sdk/pull/5501)
* Fix room list help prompt alignment
[\#5500](https://github.com/matrix-org/matrix-react-sdk/pull/5500)
Changes in [3.11.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.1) (2020-12-21)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0...v3.11.1)
* Upgrade JS SDK to 9.4.1
Changes in [3.11.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0) (2020-12-21)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0-rc.2...v3.11.0)
* Upgrade JS SDK to 9.4.0
* [Release] Look for emoji in the body that will be displayed
[\#5519](https://github.com/matrix-org/matrix-react-sdk/pull/5519)
* [Release] Recognise `*.element.io` links as Element permalinks
[\#5516](https://github.com/matrix-org/matrix-react-sdk/pull/5516)
* [Release] Fixes for call UI
[\#5513](https://github.com/matrix-org/matrix-react-sdk/pull/5513)
* [RELEASE] Add a snowfall chat effect (with /snowfall command)
[\#5512](https://github.com/matrix-org/matrix-react-sdk/pull/5512)
* [Release] Fix padding in confirmation email registration prompt
[\#5502](https://github.com/matrix-org/matrix-react-sdk/pull/5502)
Changes in [3.11.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0-rc.2) (2020-12-16)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.11.0-rc.1...v3.11.0-rc.2)
* Upgrade JS SDK to 9.4.0-rc.2
Changes in [3.11.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.11.0-rc.1) (2020-12-16)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0...v3.11.0-rc.1)
* Upgrade JS SDK to 9.4.0-rc.1
* Translations update from Weblate
[\#5497](https://github.com/matrix-org/matrix-react-sdk/pull/5497)
* Unregister from the dispatcher in CallHandler
[\#5495](https://github.com/matrix-org/matrix-react-sdk/pull/5495)
* Better adhere to MSC process
[\#5496](https://github.com/matrix-org/matrix-react-sdk/pull/5496)
* Use random pickle key on all platforms
[\#5483](https://github.com/matrix-org/matrix-react-sdk/pull/5483)
* Fix mx_MemberList icons
[\#5492](https://github.com/matrix-org/matrix-react-sdk/pull/5492)
* Convert InviteDialog to TypeScript
[\#5491](https://github.com/matrix-org/matrix-react-sdk/pull/5491)
* Add keyboard shortcut for emoji reactions
[\#5425](https://github.com/matrix-org/matrix-react-sdk/pull/5425)
* Run chat effects on events sent by widgets too
[\#5488](https://github.com/matrix-org/matrix-react-sdk/pull/5488)
* Fix being unable to pin widgets
[\#5487](https://github.com/matrix-org/matrix-react-sdk/pull/5487)
* Line 1 / 2 Support
[\#5468](https://github.com/matrix-org/matrix-react-sdk/pull/5468)
* Remove impossible labs feature: sending hidden read receipts
[\#5484](https://github.com/matrix-org/matrix-react-sdk/pull/5484)
* Fix height of Remote Video in call
[\#5456](https://github.com/matrix-org/matrix-react-sdk/pull/5456)
* Add UI for hold functionality
[\#5446](https://github.com/matrix-org/matrix-react-sdk/pull/5446)
* Allow SearchBox to expand to fill width
[\#5411](https://github.com/matrix-org/matrix-react-sdk/pull/5411)
* Use room alias in generated permalink for rooms
[\#5451](https://github.com/matrix-org/matrix-react-sdk/pull/5451)
* Only show confetti if the current room is receiving an appropriate event
[\#5482](https://github.com/matrix-org/matrix-react-sdk/pull/5482)
* Throttle RoomState.members handler to improve performance
[\#5481](https://github.com/matrix-org/matrix-react-sdk/pull/5481)
* Handle manual hs urls better for the server picker
[\#5477](https://github.com/matrix-org/matrix-react-sdk/pull/5477)
* Add Olm as a dev dependency for types
[\#5479](https://github.com/matrix-org/matrix-react-sdk/pull/5479)
* Hide Invite to this room CTA if no permission
[\#5476](https://github.com/matrix-org/matrix-react-sdk/pull/5476)
* Fix width of underline in server picker dialog
[\#5478](https://github.com/matrix-org/matrix-react-sdk/pull/5478)
* Fix confetti room unread state check
[\#5475](https://github.com/matrix-org/matrix-react-sdk/pull/5475)
* Show confetti in a chat room on command or emoji
[\#5140](https://github.com/matrix-org/matrix-react-sdk/pull/5140)
* Fix inverted settings default value
[\#5391](https://github.com/matrix-org/matrix-react-sdk/pull/5391)
* Improve usability of the Server Picker Dialog
[\#5474](https://github.com/matrix-org/matrix-react-sdk/pull/5474)
* Fix typos in some strings
[\#5473](https://github.com/matrix-org/matrix-react-sdk/pull/5473)
* Bump highlight.js from 10.1.2 to 10.4.1
[\#5472](https://github.com/matrix-org/matrix-react-sdk/pull/5472)
* Remove old app test script path
[\#5471](https://github.com/matrix-org/matrix-react-sdk/pull/5471)
* add support for giving reason when redacting
[\#5260](https://github.com/matrix-org/matrix-react-sdk/pull/5260)
* Add support for Netlify to fetchdep script
[\#5469](https://github.com/matrix-org/matrix-react-sdk/pull/5469)
* Nest other layers inside on automation
[\#5467](https://github.com/matrix-org/matrix-react-sdk/pull/5467)
* Rebrand various CI scripts and modules
[\#5466](https://github.com/matrix-org/matrix-react-sdk/pull/5466)
* Add more widget sanity checking
[\#5462](https://github.com/matrix-org/matrix-react-sdk/pull/5462)
* Fix React complaining about unknown DOM props
[\#5465](https://github.com/matrix-org/matrix-react-sdk/pull/5465)
* Jump to home page when leaving a room
[\#5464](https://github.com/matrix-org/matrix-react-sdk/pull/5464)
* Fix SSO buttons for Social Logins
[\#5463](https://github.com/matrix-org/matrix-react-sdk/pull/5463)
* Social Login and login delight tweaks
[\#5426](https://github.com/matrix-org/matrix-react-sdk/pull/5426)
Changes in [3.10.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0) (2020-12-07)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.10.0-rc.1...v3.10.0)
* Upgrade to JS SDK 9.3.0
Changes in [3.10.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.10.0-rc.1) (2020-12-02)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0...v3.10.0-rc.1)
* Upgrade to JS SDK 9.3.0-rc.1
* Translations update from Weblate
[\#5461](https://github.com/matrix-org/matrix-react-sdk/pull/5461)
* Fix VoIP call plinth on dark theme
[\#5460](https://github.com/matrix-org/matrix-react-sdk/pull/5460)
* Add sanity checking around widget pinning
[\#5459](https://github.com/matrix-org/matrix-react-sdk/pull/5459)
* Update i18n for Appearance User Settings
[\#5457](https://github.com/matrix-org/matrix-react-sdk/pull/5457)
* Only show 'answered elsewhere' if we tried to answer too
[\#5455](https://github.com/matrix-org/matrix-react-sdk/pull/5455)
* Fixed Avatar for 3PID invites
[\#5442](https://github.com/matrix-org/matrix-react-sdk/pull/5442)
* Slightly better error if we can't capture user media
[\#5449](https://github.com/matrix-org/matrix-react-sdk/pull/5449)
* Make it possible in-code to hide rooms from the room list
[\#5445](https://github.com/matrix-org/matrix-react-sdk/pull/5445)
* Fix the stickerpicker
[\#5447](https://github.com/matrix-org/matrix-react-sdk/pull/5447)
* Add live password validation to change password dialog
[\#5436](https://github.com/matrix-org/matrix-react-sdk/pull/5436)
* LaTeX rendering in element-web using KaTeX
[\#5244](https://github.com/matrix-org/matrix-react-sdk/pull/5244)
* Add lifecycle customisation point after logout
[\#5448](https://github.com/matrix-org/matrix-react-sdk/pull/5448)
* Simplify UserMenu for Guests as they can't use most of the options
[\#5421](https://github.com/matrix-org/matrix-react-sdk/pull/5421)
* Fix known issues with modal widgets
[\#5444](https://github.com/matrix-org/matrix-react-sdk/pull/5444)
* Fix existing widgets not having approved capabilities for their function
[\#5443](https://github.com/matrix-org/matrix-react-sdk/pull/5443)
* Use the WidgetDriver to run OIDC requests
[\#5440](https://github.com/matrix-org/matrix-react-sdk/pull/5440)
* Add a customisation point for widget permissions and fix amnesia issues
[\#5439](https://github.com/matrix-org/matrix-react-sdk/pull/5439)
* Fix Widget event notification text including spurious space
[\#5441](https://github.com/matrix-org/matrix-react-sdk/pull/5441)
* Move call listener out of MatrixChat
[\#5438](https://github.com/matrix-org/matrix-react-sdk/pull/5438)
* New Look in-Call View
[\#5432](https://github.com/matrix-org/matrix-react-sdk/pull/5432)
* Support arbitrary widgets sticking to the screen + sending stickers
[\#5435](https://github.com/matrix-org/matrix-react-sdk/pull/5435)
* Auth typescripting and validation tweaks
[\#5433](https://github.com/matrix-org/matrix-react-sdk/pull/5433)
* Add new widget API actions for changing rooms and sending/receiving events
[\#5385](https://github.com/matrix-org/matrix-react-sdk/pull/5385)
* Revert room header click behaviour to opening room settings
[\#5434](https://github.com/matrix-org/matrix-react-sdk/pull/5434)
* Add option to send/edit a message with Ctrl + Enter / Command + Enter
[\#5160](https://github.com/matrix-org/matrix-react-sdk/pull/5160)
* Add Analytics instrumentation to the Homepage
[\#5409](https://github.com/matrix-org/matrix-react-sdk/pull/5409)
* Fix encrypted video playback in Chrome-based browsers
[\#5430](https://github.com/matrix-org/matrix-react-sdk/pull/5430)
* Add border-radius for video
[\#5333](https://github.com/matrix-org/matrix-react-sdk/pull/5333)
* Push name to the end, near text, in IRC layout
[\#5166](https://github.com/matrix-org/matrix-react-sdk/pull/5166)
* Disable notifications for the room you have recently been active in
[\#5325](https://github.com/matrix-org/matrix-react-sdk/pull/5325)
* Search through the list of unfiltered rooms rather than the rooms in the
state which are already filtered by the search text
[\#5331](https://github.com/matrix-org/matrix-react-sdk/pull/5331)
* Lighten blockquote colour in dark mode
[\#5353](https://github.com/matrix-org/matrix-react-sdk/pull/5353)
* Specify community description img must be mxc urls
[\#5364](https://github.com/matrix-org/matrix-react-sdk/pull/5364)
* Add keyboard shortcut to close the current conversation
[\#5253](https://github.com/matrix-org/matrix-react-sdk/pull/5253)
* Redirect user home from auth screens if they are already logged in
[\#5423](https://github.com/matrix-org/matrix-react-sdk/pull/5423)
Changes in [3.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0) (2020-11-23) Changes in [3.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0) (2020-11-23)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0-rc.1...v3.9.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0-rc.1...v3.9.0)

View file

@ -35,12 +35,6 @@ General Style
- lowerCamelCase for functions and variables. - lowerCamelCase for functions and variables.
- Single line ternary operators are fine. - Single line ternary operators are fine.
- UPPER_SNAKE_CASE for constants - UPPER_SNAKE_CASE for constants
- Single quotes for strings by default, for consistency with most JavaScript styles:
```javascript
"bad" // Bad
'good' // Good
```
- Use parentheses or `` ` `` instead of `\` for line continuation where ever possible - Use parentheses or `` ` `` instead of `\` for line continuation where ever possible
- Open braces on the same line (consistent with Node): - Open braces on the same line (consistent with Node):
@ -162,7 +156,14 @@ ECMAScript
- Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an - Be careful mixing arrow functions and regular functions, eg. if one function in a promise chain is an
arrow function, they probably all should be. arrow function, they probably all should be.
- Apart from that, newer ES features should be used whenever the author deems them to be appropriate. - Apart from that, newer ES features should be used whenever the author deems them to be appropriate.
- Flow annotations are welcome and encouraged.
TypeScript
----------
- TypeScript is preferred over the use of JavaScript
- It's desirable to convert existing JavaScript files to TypeScript. TypeScript conversions should be done in small
chunks without functional changes to ease the review process.
- Use full type definitions for function parameters and return values.
- Avoid `any` types and `any` casts
React React
----- -----
@ -201,6 +202,8 @@ React
this.state = { counter: 0 }; this.state = { counter: 0 };
} }
``` ```
- Prefer class components over function components and hooks (not a strict rule though)
- Think about whether your component really needs state: are you duplicating - Think about whether your component really needs state: are you duplicating
information in component state that could be derived from the model? information in component state that could be derived from the model?

60
docs/widget-layouts.md Normal file
View file

@ -0,0 +1,60 @@
# Widget layout support
Rooms can have a default widget layout to auto-pin certain widgets, make the container different
sizes, etc. These are defined through the `io.element.widgets.layout` state event (empty state key).
Full example content:
```json5
{
"widgets": {
"first-widget-id": {
"container": "top",
"index": 0,
"width": 60,
"height": 40
},
"second-widget-id": {
"container": "right"
}
}
}
```
As shown, there are two containers possible for widgets. These containers have different behaviour
and interpret the other options differently.
## `top` container
This is the "App Drawer" or any pinned widgets in a room. This is by far the most versatile container
though does introduce potential usability issues upon members of the room (widgets take up space and
therefore fewer messages can be shown).
The `index` for a widget determines which order the widgets show up in from left to right. Widgets
without an `index` will show up as the rightmost widgets. Tiebreaks (same `index` or multiple defined
without an `index`) are resolved by comparing widget IDs. A maximum of 3 widgets can be in the top
container - any which exceed this will be ignored (placed into the `right` container). Smaller numbers
represent leftmost widgets.
The `width` is relative width within the container in percentage points. This will be clamped to a
range of 0-100 (inclusive). The widgets will attempt to scale to relative proportions when more than
100% space is allocated. For example, if 3 widgets are defined at 40% width each then the client will
attempt to show them at 33% width each.
Note that the client may impose minimum widths on the widgets, such as a 10% minimum to avoid pinning
hidden widgets. In general, widgets defined in the 30-70% range each will be free of these restrictions.
The `height` is not in fact applied per-widget but is recorded per-widget for potential future
capabilities in future containers. The top container will take the tallest `height` and use that for
the height of the whole container, and thus all widgets in that container. The `height` is relative
to the container, like with `width`, meaning that 100% will consume as much space as the client is
willing to sacrifice to the widget container. Like with `width`, the client may impose minimums to avoid
the container being uselessly small. Heights in the 30-100% range are generally acceptable. The height
is also clamped to be within 0-100, inclusive.
## `right` container
This is the default container and has no special configuration. Widgets which overflow from the top
container will be put in this container instead. Putting a widget in the right container does not
automatically show it - it only mentions that widgets should not be in another container.
The behaviour of this container may change in the future.

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.9.0", "version": "3.14.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -27,11 +27,12 @@
"matrix-gen-i18n": "scripts/gen-i18n.js", "matrix-gen-i18n": "scripts/gen-i18n.js",
"matrix-prune-i18n": "scripts/prune-i18n.js" "matrix-prune-i18n": "scripts/prune-i18n.js"
}, },
"main": "./lib/index.js", "main": "./src/index.js",
"typings": "./lib/index.d.ts",
"matrix_src_main": "./src/index.js", "matrix_src_main": "./src/index.js",
"matrix_lib_main": "./lib/index.js",
"matrix_lib_typings": "./lib/index.d.ts",
"scripts": { "scripts": {
"prepare": "yarn build", "prepublishOnly": "yarn build",
"i18n": "matrix-gen-i18n", "i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n", "prunei18n": "matrix-prune-i18n",
"diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json", "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
@ -50,51 +51,49 @@
"lint:types": "tsc --noEmit --jsx react", "lint:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'", "lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest", "test": "jest",
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080" "test:e2e": "./test/end-to-end-tests/run.sh --app-url http://localhost:8080"
}, },
"dependencies": { "dependencies": {
"@babel/runtime": "^7.10.5", "@babel/runtime": "^7.12.5",
"await-lock": "^2.0.1", "await-lock": "^2.1.0",
"blueimp-canvas-to-blob": "^3.27.0", "blueimp-canvas-to-blob": "^3.28.0",
"browser-encrypt-attachment": "^0.3.0", "browser-encrypt-attachment": "^0.3.0",
"browser-request": "^0.3.3", "browser-request": "^0.3.3",
"cheerio": "^1.0.0-rc.5",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"commonmark": "^0.29.1", "commonmark": "^0.29.3",
"counterpart": "^0.18.6", "counterpart": "^0.18.6",
"diff-dom": "^4.1.6", "diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5", "diff-match-patch": "^1.0.5",
"emojibase-data": "^5.0.1", "emojibase-data": "^5.1.1",
"emojibase-regex": "^4.0.1", "emojibase-regex": "^4.1.1",
"escape-html": "^1.0.3", "escape-html": "^1.0.3",
"file-saver": "^1.3.8", "file-saver": "^2.0.5",
"filesize": "3.6.1", "filesize": "6.1.0",
"flux": "2.1.1", "flux": "2.1.1",
"focus-visible": "^5.1.0", "focus-visible": "^5.2.0",
"fuse.js": "^2.7.4",
"gfm.css": "^1.1.2", "gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1", "glob-to-regexp": "^0.4.1",
"highlight.js": "^10.1.2", "highlight.js": "^10.5.0",
"html-entities": "^1.3.1", "html-entities": "^1.4.0",
"is-ip": "^2.0.0", "is-ip": "^3.1.0",
"katex": "^0.12.0", "katex": "^0.12.0",
"cheerio": "^1.0.0-rc.3",
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.19", "lodash": "^4.17.20",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.10", "matrix-widget-api": "^0.1.0-beta.13",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"pako": "^1.0.11", "pako": "^2.0.3",
"parse5": "^5.1.1", "parse5": "^6.0.1",
"png-chunks-extract": "^1.0.0", "png-chunks-extract": "^1.0.0",
"project-name-generator": "^2.1.7",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"qs": "^6.9.4", "qs": "^6.9.6",
"re-resizable": "^6.5.4", "re-resizable": "^6.9.0",
"react": "^16.13.1", "react": "^16.14.0",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.13.1", "react-dom": "^16.14.0",
"react-focus-lock": "^2.4.1", "react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.1", "resize-observer-polyfill": "^1.5.1",
"rfc4648": "^1.4.0", "rfc4648": "^1.4.0",
@ -107,70 +106,75 @@
"zxcvbn": "^4.4.2" "zxcvbn": "^4.4.2"
}, },
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.10.5", "@babel/cli": "^7.12.10",
"@babel/core": "^7.10.5", "@babel/core": "^7.12.10",
"@babel/parser": "^7.11.0", "@babel/parser": "^7.12.11",
"@babel/plugin-proposal-class-properties": "^7.10.4", "@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-decorators": "^7.10.5", "@babel/plugin-proposal-decorators": "^7.12.12",
"@babel/plugin-proposal-export-default-from": "^7.10.4", "@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.10.4", "@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.10.4", "@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-flow-comments": "^7.10.4", "@babel/plugin-transform-flow-comments": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.10.5", "@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.10.4", "@babel/preset-env": "^7.12.11",
"@babel/preset-flow": "^7.10.4", "@babel/preset-flow": "^7.12.1",
"@babel/preset-react": "^7.10.4", "@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.10.4", "@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.10.5", "@babel/register": "^7.12.10",
"@babel/traverse": "^7.11.0", "@babel/traverse": "^7.12.12",
"@peculiar/webcrypto": "^1.1.3", "@peculiar/webcrypto": "^1.1.4",
"@types/classnames": "^2.2.10", "@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/jest": "^26.0.20",
"@types/linkifyjs": "^2.1.3", "@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.158", "@types/lodash": "^4.14.168",
"@types/modernizr": "^3.5.3", "@types/modernizr": "^3.5.3",
"@types/node": "^12.12.51", "@types/node": "^14.14.22",
"@types/pako": "^1.0.1", "@types/pako": "^1.0.1",
"@types/qrcode": "^1.3.4", "@types/qrcode": "^1.3.5",
"@types/react": "^16.9", "@types/react": "^16.9",
"@types/react-dom": "^16.9.8", "@types/react-dom": "^16.9.10",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^1.23.3", "@types/sanitize-html": "^1.27.0",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.7.0", "@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^3.7.0", "@typescript-eslint/parser": "^4.14.0",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^24.9.0", "babel-jest": "^26.6.3",
"chokidar": "^3.4.1", "chokidar": "^3.5.1",
"concurrently": "^4.1.2", "concurrently": "^5.3.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2", "enzyme-adapter-react-16": "^1.15.6",
"eslint": "7.5.0", "eslint": "7.18.0",
"eslint-config-matrix-org": "^0.1.2", "eslint-config-matrix-org": "^0.2.0",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
"eslint-plugin-flowtype": "^2.50.3", "eslint-plugin-flowtype": "^5.2.0",
"eslint-plugin-react": "^7.20.3", "eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^2.5.1", "eslint-plugin-react-hooks": "^4.2.0",
"glob": "^5.0.15", "glob": "^7.1.6",
"jest": "^26.5.2", "jest": "^26.6.3",
"jest-canvas-mock": "^2.3.0", "jest-canvas-mock": "^2.3.0",
"jest-environment-jsdom-sixteen": "^1.0.3", "jest-environment-jsdom-sixteen": "^1.0.3",
"lolex": "^5.1.2",
"matrix-mock-request": "^1.2.3", "matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2", "matrix-react-test-utils": "^0.2.2",
"react-test-renderer": "^16.13.1", "olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"rimraf": "^2.7.1", "react-test-renderer": "^16.14.0",
"stylelint": "^9.10.1", "rimraf": "^3.0.2",
"stylelint-config-standard": "^18.3.0", "stylelint": "^13.9.0",
"stylelint-config-standard": "^20.0.0",
"stylelint-scss": "^3.18.0", "stylelint-scss": "^3.18.0",
"typescript": "^3.9.7", "typescript": "^4.1.3",
"walk": "^2.3.14" "walk": "^2.3.14"
}, },
"resolutions": {
"**/@types/react": "^16.14"
},
"jest": { "jest": {
"testEnvironment": "./__test-utils__/environment.js", "testEnvironment": "./__test-utils__/environment.js",
"testMatch": [ "testMatch": [
"<rootDir>/test/**/*-test.js" "<rootDir>/test/**/*-test.[jt]s"
], ],
"setupFiles": [ "setupFiles": [
"jest-canvas-mock" "jest-canvas-mock"

View file

@ -21,6 +21,11 @@ limitations under the License.
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic $hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
$EventTile_e2e_state_indicator_width: 4px;
$MessageTimestamp_width: 46px; /* 8 + 30 (avatar) + 8 */
$MessageTimestamp_width_hover: calc($MessageTimestamp_width - 2 * $EventTile_e2e_state_indicator_width);
:root { :root {
font-size: 10px; font-size: 10px;
} }
@ -170,7 +175,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
border: 1px solid rgba($primary-fg-color, .1); border: 1px solid rgba($primary-fg-color, .1);
// these things should probably not be defined globally // these things should probably not be defined globally
margin: 9px; margin: 9px;
flex: 0 0 auto;
} }
.mx_textinput { .mx_textinput {

View file

@ -51,6 +51,7 @@
@import "./views/avatars/_MemberStatusMessageAvatar.scss"; @import "./views/avatars/_MemberStatusMessageAvatar.scss";
@import "./views/avatars/_PulsedAvatar.scss"; @import "./views/avatars/_PulsedAvatar.scss";
@import "./views/avatars/_WidgetAvatar.scss"; @import "./views/avatars/_WidgetAvatar.scss";
@import "./views/context_menus/_CallContextMenu.scss";
@import "./views/context_menus/_IconizedContextMenu.scss"; @import "./views/context_menus/_IconizedContextMenu.scss";
@import "./views/context_menus/_MessageContextMenu.scss"; @import "./views/context_menus/_MessageContextMenu.scss";
@import "./views/context_menus/_StatusMessageContextMenu.scss"; @import "./views/context_menus/_StatusMessageContextMenu.scss";
@ -70,6 +71,7 @@
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss"; @import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.scss";
@import "./views/dialogs/_HostSignupDialog.scss";
@import "./views/dialogs/_IncomingSasDialog.scss"; @import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss"; @import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss"; @import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@ -105,6 +107,7 @@
@import "./views/elements/_AddressTile.scss"; @import "./views/elements/_AddressTile.scss";
@import "./views/elements/_DesktopBuildsNotice.scss"; @import "./views/elements/_DesktopBuildsNotice.scss";
@import "./views/elements/_DirectorySearchBox.scss"; @import "./views/elements/_DirectorySearchBox.scss";
@import "./views/elements/_DesktopCapturerSourcePicker.scss";
@import "./views/elements/_Dropdown.scss"; @import "./views/elements/_Dropdown.scss";
@import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_EditableItemList.scss";
@import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_ErrorBoundary.scss";
@ -236,4 +239,7 @@
@import "./views/verification/_VerificationShowSas.scss"; @import "./views/verification/_VerificationShowSas.scss";
@import "./views/voip/_CallContainer.scss"; @import "./views/voip/_CallContainer.scss";
@import "./views/voip/_CallView.scss"; @import "./views/voip/_CallView.scss";
@import "./views/voip/_DialPad.scss";
@import "./views/voip/_DialPadContextMenu.scss";
@import "./views/voip/_DialPadModal.scss";
@import "./views/voip/_VideoFeed.scss"; @import "./views/voip/_VideoFeed.scss";

View file

@ -180,6 +180,11 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
.mx_LeftPanel_roomListContainer { .mx_LeftPanel_roomListContainer {
width: 68px; width: 68px;
.mx_LeftPanel_userHeader {
flex-direction: row;
justify-content: center;
}
.mx_LeftPanel_filterContainer { .mx_LeftPanel_filterContainer {
// Organize the flexbox into a centered column layout // Organize the flexbox into a centered column layout
flex-direction: column; flex-direction: column;

View file

@ -134,7 +134,7 @@ limitations under the License.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); mask-image: url('$(res)/img/feather-customised/maximise.svg');
background: $muted-fg-color; background: $muted-fg-color;
} }
} }

View file

@ -64,28 +64,23 @@ limitations under the License.
} }
.mx_RoomDirectory_table { .mx_RoomDirectory_table {
font-size: $font-12px;
color: $primary-fg-color; color: $primary-fg-color;
width: 100%; display: grid;
font-size: $font-12px;
grid-template-columns: max-content auto max-content max-content max-content;
row-gap: 24px;
text-align: left; text-align: left;
table-layout: fixed; width: 100%;
} }
.mx_RoomDirectory_roomAvatar { .mx_RoomDirectory_roomAvatar {
width: 32px; padding: 2px 14px 0 0;
padding-right: 14px;
vertical-align: top;
}
.mx_RoomDirectory_roomDescription {
padding-bottom: 16px;
} }
.mx_RoomDirectory_roomMemberCount { .mx_RoomDirectory_roomMemberCount {
align-self: center;
color: $light-fg-color; color: $light-fg-color;
width: 60px; padding: 3px 10px 0;
padding: 0 10px;
text-align: center;
&::before { &::before {
background-color: $light-fg-color; background-color: $light-fg-color;
@ -105,8 +100,7 @@ limitations under the License.
} }
.mx_RoomDirectory_join, .mx_RoomDirectory_preview { .mx_RoomDirectory_join, .mx_RoomDirectory_preview {
width: 80px; align-self: center;
text-align: center;
white-space: nowrap; white-space: nowrap;
} }

View file

@ -219,7 +219,7 @@ hr.mx_RoomView_myReadMarker {
position: relative; position: relative;
top: -1px; top: -1px;
z-index: 1; z-index: 1;
transition: width 400ms easeInSine 1s, opacity 400ms easeInSine 1s; transition: width 400ms easeinsine 1s, opacity 400ms easeinsine 1s;
width: 99%; width: 99%;
opacity: 1; opacity: 1;
} }

View file

@ -119,20 +119,16 @@ limitations under the License.
} }
&.mx_UserMenu_minimized { &.mx_UserMenu_minimized {
.mx_UserMenu_userHeader { padding-right: 0px;
.mx_UserMenu_row {
justify-content: center;
}
.mx_UserMenu_userAvatarContainer { .mx_UserMenu_userAvatarContainer {
margin-right: 0; margin-right: 0px;
}
} }
} }
} }
.mx_UserMenu_contextMenu { .mx_UserMenu_contextMenu {
width: 247px; width: 258px;
// These override the styles already present on the user menu rather than try to // 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 // define a new menu. They are specifically for the stacked menu when a community
@ -276,6 +272,9 @@ limitations under the License.
.mx_UserMenu_iconHome::before { .mx_UserMenu_iconHome::before {
mask-image: url('$(res)/img/element-icons/roomlist/home.svg'); mask-image: url('$(res)/img/element-icons/roomlist/home.svg');
} }
.mx_UserMenu_iconHosting::before {
mask-image: url('$(res)/img/element-icons/brands/element.svg');
}
.mx_UserMenu_iconBell::before { .mx_UserMenu_iconBell::before {
mask-image: url('$(res)/img/element-icons/notifications.svg'); mask-image: url('$(res)/img/element-icons/notifications.svg');

View file

@ -81,6 +81,7 @@ limitations under the License.
} }
.mx_Login_underlinedServerName { .mx_Login_underlinedServerName {
width: max-content;
border-bottom: 1px dashed $accent-color; border-bottom: 1px dashed $accent-color;
} }

View file

@ -34,7 +34,7 @@ limitations under the License.
h3 { h3 {
font-size: $font-14px; font-size: $font-14px;
font-weight: 600; font-weight: 600;
color: $authpage-primary-color; color: $authpage-secondary-color;
} }
h3.mx_AuthBody_centered { h3.mx_AuthBody_centered {

View file

@ -18,7 +18,7 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 206px; width: 206px;
padding: 25px 40px; padding: 25px 25px;
box-sizing: border-box; box-sizing: border-box;
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
.mx_AuthHeaderLogo { .mx_AuthHeaderLogo {
margin-top: 15px; margin-top: 15px;
flex: 1; flex: 1;
padding: 0 10px; padding: 0 25px;
} }
.mx_AuthHeaderLogo img { .mx_AuthHeaderLogo img {

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
.mx_InteractiveAuthEntryComponents_emailWrapper { .mx_InteractiveAuthEntryComponents_emailWrapper {
padding-right: 60px; padding-right: 100px;
position: relative; position: relative;
margin-top: 32px; margin-top: 32px;
margin-bottom: 32px; margin-bottom: 32px;
@ -83,7 +83,10 @@ limitations under the License.
} }
.mx_InteractiveAuthEntryComponents_termsPolicy { .mx_InteractiveAuthEntryComponents_termsPolicy {
display: block; display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
} }
.mx_InteractiveAuthEntryComponents_passwordSection { .mx_InteractiveAuthEntryComponents_passwordSection {

View file

@ -23,6 +23,7 @@ limitations under the License.
font-size: $font-14px; font-size: $font-14px;
font-weight: 600; font-weight: 600;
color: $authpage-lang-color; color: $authpage-lang-color;
width: auto;
} }
.mx_AuthBody_language .mx_Dropdown_arrow { .mx_AuthBody_language .mx_Dropdown_arrow {

View file

@ -18,7 +18,6 @@ limitations under the License.
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
&.mx_WelcomePage_registrationDisabled { &.mx_WelcomePage_registrationDisabled {
.mx_ButtonCreateAccount { .mx_ButtonCreateAccount {
display: none; display: none;
@ -27,6 +26,6 @@ limitations under the License.
} }
.mx_Welcome .mx_AuthBody_language { .mx_Welcome .mx_AuthBody_language {
width: 120px; width: 160px;
margin-bottom: 10px; margin-bottom: 10px;
} }

View file

@ -0,0 +1,23 @@
/*
Copyright 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.
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_CallContextMenu_item {
width: 205px;
height: 40px;
padding-left: 16px;
line-height: 40px;
vertical-align: center;
}

View file

@ -0,0 +1,138 @@
/*
Copyright 2021 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_HostSignupDialog {
width: 90vw;
max-width: 580px;
height: 80vh;
max-height: 600px;
.mx_HostSignupDialog_info {
text-align: center;
.mx_HostSignupDialog_content_top {
margin-bottom: 24px;
}
.mx_HostSignupDialog_paragraphs {
text-align: left;
padding-left: 25%;
padding-right: 25%;
}
.mx_HostSignupDialog_buttons {
margin-bottom: 24px;
display: flex;
justify-content: center;
button {
padding: 12px;
margin: 0 16px;
}
}
.mx_HostSignupDialog_footer {
display: flex;
justify-content: center;
align-items: baseline;
img {
padding-right: 5px;
}
}
}
iframe {
width: 100%;
height: 100%;
border: none;
background-color: #fff;
min-height: 540px;
}
}
.mx_HostSignupDialog_text_dark {
color: $primary-fg-color;
}
.mx_HostSignupDialog_text_light {
color: $secondary-fg-color;
}
.mx_HostSignup_maximize_button {
mask: url('$(res)/img/feather-customised/maximise.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
position: absolute;
top: 10px;
right: 10px;
}
.mx_HostSignup_minimize_button {
mask: url('$(res)/img/feather-customised/minimise.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
position: absolute;
top: 10px;
right: 25px;
}
.mx_HostSignup_persisted {
width: 90vw;
max-width: 580px;
height: 80vh;
max-height: 600px;
top: 0;
left: 0;
position: fixed;
display: none;
}
.mx_HostSignupDialog_minimized {
position: fixed;
bottom: 80px;
right: 26px;
width: 314px;
height: 217px;
overflow: hidden;
&.mx_Dialog {
padding: 12px;
}
.mx_Dialog_title {
text-align: left !important;
padding-left: 20px;
font-size: $font-15px;
}
iframe {
width: 100%;
height: 100%;
border: none;
background-color: #fff;
}
}

View file

@ -89,24 +89,18 @@ limitations under the License.
} }
} }
.mx_showMore {
display: block;
text-align: left;
margin-top: 10px;
}
.metadata { .metadata {
color: $muted-fg-color; color: $muted-fg-color;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-bottom: 0; margin-bottom: 0;
}
.metadata.visible {
overflow-y: visible; overflow-y: visible;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: normal; white-space: normal;
padding: 0;
> li {
padding: 0;
border: 0;
}
} }
} }
} }

View file

@ -0,0 +1,72 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
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_desktopCapturerSourcePicker {
overflow: hidden;
}
.mx_desktopCapturerSourcePicker_tabLabels {
display: flex;
padding: 0 0 8px 0;
}
.mx_desktopCapturerSourcePicker_tabLabel,
.mx_desktopCapturerSourcePicker_tabLabel_selected {
width: 100%;
text-align: center;
border-radius: 8px;
padding: 8px 0;
font-size: $font-13px;
}
.mx_desktopCapturerSourcePicker_tabLabel_selected {
background-color: $tab-label-active-bg-color;
color: $tab-label-active-fg-color;
}
.mx_desktopCapturerSourcePicker_panel {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
height: 500px;
overflow: overlay;
}
.mx_desktopCapturerSourcePicker_stream_button {
display: flex;
flex-direction: column;
margin: 8px;
border-radius: 4px;
}
.mx_desktopCapturerSourcePicker_stream_button:hover,
.mx_desktopCapturerSourcePicker_stream_button:focus {
background: $roomtile-selected-bg-color;
}
.mx_desktopCapturerSourcePicker_stream_thumbnail {
margin: 4px;
width: 312px;
}
.mx_desktopCapturerSourcePicker_stream_name {
margin: 0 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 312px;
}

View file

@ -18,6 +18,16 @@ limitations under the License.
position: relative; position: relative;
width: min-content; width: min-content;
// this isn't a floating tooltip so override some things to not need to bother with z-index and floating
.mx_Tooltip {
display: inline-block;
position: absolute;
z-index: unset;
width: max-content;
left: 72px;
top: 0;
}
&::before, &::after { &::before, &::after {
content: ''; content: '';
position: absolute; position: absolute;

View file

@ -16,13 +16,26 @@ limitations under the License.
.mx_SSOButtons { .mx_SSOButtons {
display: flex; display: flex;
flex-wrap: wrap;
justify-content: center; justify-content: center;
.mx_SSOButtons_row {
& + .mx_SSOButtons_row {
margin-top: 16px;
}
}
.mx_SSOButton { .mx_SSOButton {
position: relative; position: relative;
width: 100%; width: 100%;
padding-left: 32px; padding: 7px 32px;
padding-right: 32px; text-align: center;
border-radius: 8px;
display: inline-block;
font-size: $font-14px;
font-weight: $font-semi-bold;
border: 1px solid $input-border-color;
color: $primary-fg-color;
> img { > img {
object-fit: contain; object-fit: contain;
@ -32,10 +45,22 @@ limitations under the License.
} }
} }
.mx_SSOButton_default {
color: $button-primary-bg-color;
background-color: $button-secondary-bg-color;
border-color: $button-primary-bg-color;
}
.mx_SSOButton_default.mx_SSOButton_primary {
color: $button-primary-fg-color;
background-color: $button-primary-bg-color;
}
.mx_SSOButton_mini { .mx_SSOButton_mini {
box-sizing: border-box; box-sizing: border-box;
width: 50px; // 48px + 1px border on all sides width: 50px; // 48px + 1px border on all sides
height: 50px; // 48px + 1px border on all sides height: 50px; // 48px + 1px border on all sides
min-width: 50px; // prevent crushing by the flexbox
padding: 12px;
> img { > img {
left: 12px; left: 12px;
@ -43,7 +68,7 @@ limitations under the License.
} }
& + .mx_SSOButton_mini { & + .mx_SSOButton_mini {
margin-left: 24px; margin-left: 16px;
} }
} }
} }

View file

@ -59,7 +59,7 @@ limitations under the License.
} }
.mx_ServerPicker_server { .mx_ServerPicker_server {
color: $primary-fg-color; color: $authpage-primary-color;
grid-column: 1; grid-column: 1;
grid-row: 2; grid-row: 2;
margin-bottom: 16px; margin-bottom: 16px;

View file

@ -17,7 +17,7 @@ limitations under the License.
span.mx_MVideoBody { span.mx_MVideoBody {
video.mx_MVideoBody { video.mx_MVideoBody {
max-width: 100%; max-width: 100%;
height: auto; max-height: 300px;
border-radius: 4px; border-radius: 4px;
} }
} }

View file

@ -30,7 +30,7 @@ limitations under the License.
mask-size: contain; mask-size: contain;
content: ''; content: '';
position: absolute; position: absolute;
top: 2px; top: 1px;
left: 0; left: 0;
} }
} }

View file

@ -35,13 +35,13 @@ limitations under the License.
mask-size: auto 12px; mask-size: auto 12px;
visibility: hidden; visibility: hidden;
background-color: $accent-color; background-color: $accent-color;
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg'); mask-image: url('$(res)/img/feather-customised/maximise.svg');
} }
&.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle { &.mx_ViewSourceEvent_expanded .mx_ViewSourceEvent_toggle {
mask-position: 0 bottom; mask-position: 0 bottom;
margin-bottom: 7px; margin-bottom: 7px;
mask-image: url('$(res)/img/feather-customised/widget/minimise.svg'); mask-image: url('$(res)/img/feather-customised/minimise.svg');
} }
&:hover .mx_ViewSourceEvent_toggle { &:hover .mx_ViewSourceEvent_toggle {

View file

@ -24,26 +24,45 @@ $MiniAppTileHeight: 200px;
flex-direction: column; flex-direction: column;
overflow: hidden; overflow: hidden;
.mx_AppsContainer_resizerHandleContainer {
width: 100%;
height: 10px;
margin-top: -3px; // move it up so the interactions are slightly more comfortable
display: block;
position: relative;
}
.mx_AppsContainer_resizerHandle { .mx_AppsContainer_resizerHandle {
cursor: ns-resize; cursor: ns-resize;
border-radius: 3px;
// Override styles from library // Override styles from library, making the whole area the target area
width: unset !important; width: 100% !important;
height: 4px !important; height: 100% !important;
// This is positioned directly below frame // This is positioned directly below frame
position: absolute; position: absolute;
bottom: -8px !important; // override from library bottom: 0 !important; // override from library
// We then render the pill handle in an ::after to keep it in the handle's
// area without being a massive line across the screen
&::after {
content: '';
position: absolute;
border-radius: 3px;
// The combination of these two should make the pill 4px high
top: 6px;
bottom: 0;
// Together, these make the bar 64px wide // Together, these make the bar 64px wide
// These are also overridden from the library // These are also overridden from the library
left: calc(50% - 32px) !important; left: calc(50% - 32px);
right: calc(50% - 32px) !important; right: calc(50% - 32px);
}
} }
&:hover { &:hover {
.mx_AppsContainer_resizerHandle { .mx_AppsContainer_resizerHandle::after {
opacity: 0.8; opacity: 0.8;
background: $primary-fg-color; background: $primary-fg-color;
} }

View file

@ -26,7 +26,7 @@ $left-gutter: 64px;
} }
.mx_EventTile.mx_EventTile_info { .mx_EventTile.mx_EventTile_info {
padding-top: 0px; padding-top: 1px;
} }
.mx_EventTile_avatar { .mx_EventTile_avatar {
@ -37,7 +37,7 @@ $left-gutter: 64px;
} }
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar { .mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
top: $font-8px; top: $font-6px;
left: $left-gutter; left: $left-gutter;
} }
@ -74,7 +74,6 @@ $left-gutter: 64px;
margin-left: 5px; margin-left: 5px;
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
height: 16px;
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
@ -421,15 +420,15 @@ $left-gutter: 64px;
} }
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color 4px solid; border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
} }
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color 4px solid; border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
} }
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color 4px solid; border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
} }
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
@ -447,8 +446,7 @@ $left-gutter: 64px;
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: 3px; width: $MessageTimestamp_width_hover;
width: auto;
} }
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
@ -493,7 +491,6 @@ $left-gutter: 64px;
// https://github.com/vector-im/vector-web/issues/754 // https://github.com/vector-im/vector-web/issues/754
overflow-x: overlay; overflow-x: overlay;
overflow-y: visible; overflow-y: visible;
max-height: 30vh;
} }
code { code {
@ -502,6 +499,22 @@ $left-gutter: 64px;
} }
} }
.mx_EventTile_lineNumbers {
float: left;
margin: 0 0.5em 0 -1.5em;
color: gray;
}
.mx_EventTile_lineNumber {
text-align: right;
display: block;
padding-left: 1em;
}
.mx_EventTile_collapsedCodeBlock {
max-height: 30vh;
}
.mx_EventTile:hover .mx_EventTile_body pre, .mx_EventTile:hover .mx_EventTile_body pre,
.mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre { .mx_EventTile.focus-visible:focus-within .mx_EventTile_body pre {
border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter border: 1px solid #e5e5e5; // deliberate constant as we're behind an invert filter
@ -513,21 +526,42 @@ $left-gutter: 64px;
} }
// Inserted adjacent to <pre> blocks, (See TextualBody) // Inserted adjacent to <pre> blocks, (See TextualBody)
.mx_EventTile_copyButton { .mx_EventTile_button {
position: absolute; position: absolute;
display: inline-block; display: inline-block;
visibility: hidden; visibility: hidden;
cursor: pointer; cursor: pointer;
top: 6px; top: 6px;
right: 6px; right: 12px;
width: 19px; width: 19px;
height: 19px; height: 19px;
mask-image: url($copy-button-url);
background-color: $message-action-bar-fg-color; background-color: $message-action-bar-fg-color;
} }
.mx_EventTile_buttonBottom {
top: 31px;
}
.mx_EventTile_copyButton {
mask-image: url($copy-button-url);
}
.mx_EventTile_collapseButton {
mask-size: 75%;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url($collapse-button-url);
}
.mx_EventTile_expandButton {
mask-size: 75%;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url($expand-button-url);
}
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton, .mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_copyButton,
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton { .mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_copyButton,
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_collapseButton,
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_collapseButton,
.mx_EventTile_body .mx_EventTile_pre_container:focus-within .mx_EventTile_expandButton,
.mx_EventTile_body .mx_EventTile_pre_container:hover .mx_EventTile_expandButton {
visibility: visible; visibility: visible;
} }

View file

@ -20,7 +20,7 @@ $left-gutter: 64px;
.mx_GroupLayout { .mx_GroupLayout {
.mx_EventTile { .mx_EventTile {
> .mx_SenderProfile { > .mx_SenderProfile {
line-height: $font-17px; line-height: $font-20px;
padding-left: $left-gutter; padding-left: $left-gutter;
} }
@ -34,11 +34,11 @@ $left-gutter: 64px;
.mx_MessageTimestamp { .mx_MessageTimestamp {
position: absolute; position: absolute;
width: 46px; /* 8 + 30 (avatar) + 8 */ width: $MessageTimestamp_width;
} }
.mx_EventTile_line, .mx_EventTile_reply { .mx_EventTile_line, .mx_EventTile_reply {
padding-top: 3px; padding-top: 1px;
padding-bottom: 3px; padding-bottom: 3px;
line-height: $font-22px; line-height: $font-22px;
} }

View file

@ -207,6 +207,17 @@ $irc-line-height: $font-18px;
width: unset; width: unset;
max-width: var(--name-width); max-width: var(--name-width);
} }
.mx_SenderProfile_hover {
background: transparent;
> span {
> .mx_SenderProfile_name,
> .mx_SenderProfile_aux {
min-width: inherit;
}
}
}
} }
.mx_ProfileResizer { .mx_ProfileResizer {

View file

@ -19,6 +19,8 @@ limitations under the License.
margin-right: 15px; margin-right: 15px;
margin-bottom: 15px; margin-bottom: 15px;
display: flex; display: flex;
flex-direction: column;
max-width: 360px;
border-left: 4px solid $preview-widget-bar-color; border-left: 4px solid $preview-widget-bar-color;
color: $preview-widget-fg-color; color: $preview-widget-fg-color;
} }
@ -55,6 +57,9 @@ limitations under the License.
cursor: pointer; cursor: pointer;
width: 18px; width: 18px;
height: 18px; height: 18px;
padding: 0px 5px 5px 5px;
margin-left: auto;
margin-right: 0px;
img { img {
flex: 0 0 40px; flex: 0 0 40px;

View file

@ -46,6 +46,11 @@ limitations under the License.
} }
} }
.mx_GroupMemberList_query,
.mx_GroupRoomList_query {
flex: 0 0 auto;
}
.mx_MemberList_chevron { .mx_MemberList_chevron {
position: absolute; position: absolute;
right: 35px; right: 35px;
@ -59,10 +64,8 @@ limitations under the License.
flex: 1 1 0px; flex: 1 1 0px;
} }
.mx_MemberList_query, .mx_MemberList_query {
.mx_GroupMemberList_query, height: 16px;
.mx_GroupRoomList_query {
flex: 1 1 0;
// stricter rule to override the one in _common.scss // stricter rule to override the one in _common.scss
&[type="text"] { &[type="text"] {
@ -70,10 +73,6 @@ limitations under the License.
} }
} }
.mx_MemberList_query {
height: 16px;
}
.mx_MemberList_wrapper { .mx_MemberList_wrapper {
padding: 10px; padding: 10px;
} }
@ -113,10 +112,10 @@ limitations under the License.
} }
} }
.mx_MemberList_inviteCommunity span { .mx_MemberList_inviteCommunity span::before {
background-image: url('$(res)/img/icon-invite-people.svg'); mask-image: url('$(res)/img/icon-invite-people.svg');
} }
.mx_MemberList_addRoomToCommunity span { .mx_MemberList_addRoomToCommunity span::before {
background-image: url('$(res)/img/icons-room-add.svg'); mask-image: url('$(res)/img/icons-room-add.svg');
} }

View file

@ -24,6 +24,9 @@ limitations under the License.
.mx_RoomList_iconExplore::before { .mx_RoomList_iconExplore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
.mx_RoomList_iconDialpad::before {
mask-image: url('$(res)/img/element-icons/roomlist/dialpad.svg');
}
.mx_RoomList_explorePrompt { .mx_RoomList_explorePrompt {
margin: 4px 12px 4px; margin: 4px 12px 4px;
@ -41,6 +44,8 @@ limitations under the License.
padding: 0 0 0 24px; padding: 0 0 0 24px;
font-size: inherit; font-size: inherit;
margin-top: 8px; margin-top: 8px;
display: block;
text-align: start;
&::before { &::before {
content: ''; content: '';

View file

@ -197,6 +197,9 @@ limitations under the License.
.mx_RoomSublist_resizerHandles { .mx_RoomSublist_resizerHandles {
flex: 0 0 4px; flex: 0 0 4px;
display: flex;
justify-content: center;
width: 100%;
} }
// Class name comes from the ResizableBox component // Class name comes from the ResizableBox component
@ -207,17 +210,12 @@ limitations under the License.
border-radius: 3px; border-radius: 3px;
// Override styles from library // Override styles from library
width: unset !important; max-width: 64px;
height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes height: 4px !important; // Update RESIZE_HANDLE_HEIGHT if this changes
// This is positioned directly below the 'show more' button. // This is positioned directly below the 'show more' button.
position: absolute; position: relative !important;
bottom: 0 !important; // override from library bottom: 0 !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;
} }
&:hover, &.mx_RoomSublist_hasMenuOpen { &:hover, &.mx_RoomSublist_hasMenuOpen {

View file

@ -64,6 +64,7 @@ limitations under the License.
.mx_UserNotifSettings_notifTable { .mx_UserNotifSettings_notifTable {
display: table; display: table;
position: relative;
} }
.mx_UserNotifSettings_notifTable .mx_Spinner { .mx_UserNotifSettings_notifTable .mx_Spinner {

View file

@ -14,6 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_ProfileSettings_controls_topic {
& > textarea {
resize: vertical;
}
}
.mx_ProfileSettings_profile { .mx_ProfileSettings_profile {
display: flex; display: flex;
} }

View file

@ -18,10 +18,7 @@ limitations under the License.
position: absolute; position: absolute;
right: 20px; right: 20px;
bottom: 72px; bottom: 72px;
border-radius: 8px;
overflow: hidden;
z-index: 100; z-index: 100;
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
// Disable pointer events for Jitsi widgets to function. Direct // Disable pointer events for Jitsi widgets to function. Direct
// calls have their own cursor and behaviour, but we need to make // calls have their own cursor and behaviour, but we need to make
@ -49,8 +46,10 @@ limitations under the License.
.mx_IncomingCallBox { .mx_IncomingCallBox {
min-width: 250px; min-width: 250px;
background-color: $primary-bg-color; background-color: $voipcall-plinth-color;
padding: 8px; padding: 8px;
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
border-radius: 8px;
pointer-events: initial; // restore pointer events so the user can accept/decline pointer-events: initial; // restore pointer events so the user can accept/decline
cursor: pointer; cursor: pointer;

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
.mx_CallView { .mx_CallView {
border-radius: 10px; border-radius: 8px;
background-color: $voipcall-plinth-color; background-color: $voipcall-plinth-color;
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
@ -26,6 +26,7 @@ limitations under the License.
.mx_CallView_large { .mx_CallView_large {
padding-bottom: 10px; padding-bottom: 10px;
margin: 5px 5px 5px 18px;
.mx_CallView_voice { .mx_CallView_voice {
height: 360px; height: 360px;
@ -34,24 +35,140 @@ limitations under the License.
.mx_CallView_pip { .mx_CallView_pip {
width: 320px; width: 320px;
padding-bottom: 8px;
margin-top: 10px;
box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
border-radius: 8px;
.mx_CallView_voice { .mx_CallView_voice {
height: 180px; height: 180px;
} }
.mx_CallView_callControls {
bottom: 0px;
}
.mx_CallView_callControls_button {
&::before {
width: 36px;
height: 36px;
}
}
.mx_CallView_voice_holdText {
padding-top: 10px;
padding-bottom: 25px;
}
} }
.mx_CallView_voice { .mx_CallView_voice {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: $inverted-bg-color; background-color: $inverted-bg-color;
border-radius: 8px;
}
.mx_CallView_voice_avatarsContainer {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
div {
margin-left: 12px;
margin-right: 12px;
}
}
.mx_CallView_voice_hold {
// This masks the avatar image so when it's blurred, the edge is still crisp
.mx_CallView_voice_avatarContainer {
border-radius: 2000px;
overflow: hidden;
position: relative;
}
}
.mx_CallView_voice_holdText {
height: 20px;
padding-top: 20px;
padding-bottom: 15px;
color: $accent-fg-color;
.mx_AccessibleButton_hasKind {
padding: 0px;
font-weight: bold;
}
} }
.mx_CallView_video { .mx_CallView_video {
width: 100%; width: 100%;
position: relative; position: relative;
z-index: 30; z-index: 30;
border-radius: 8px;
overflow: hidden;
}
.mx_CallView_video_hold {
overflow: hidden;
// we keep these around in the DOM: it saved wiring them up again when the call
// is resumed and keeps the container the right size
.mx_VideoFeed {
visibility: hidden;
}
}
.mx_CallView_video_holdBackground {
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
background-repeat: no-repeat;
background-size: cover;
background-position: center;
filter: blur(20px);
&::after {
content: '';
display: block;
position: absolute;
width: 100%;
height: 100%;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.6);
}
}
.mx_CallView_video_holdContent {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-weight: bold;
color: $accent-fg-color;
text-align: center;
&::before {
display: block;
margin-left: auto;
margin-right: auto;
content: '';
width: 40px;
height: 40px;
background-image: url('$(res)/img/voip/paused.svg');
background-position: center;
background-size: cover;
}
.mx_CallView_pip &::before {
width: 30px;
height: 30px;
}
.mx_AccessibleButton_hasKind {
padding: 0px;
}
} }
.mx_CallView_header { .mx_CallView_header {
@ -60,17 +177,22 @@ limitations under the License.
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
justify-content: left; justify-content: left;
.mx_BaseAvatar {
margin-right: 12px;
}
} }
.mx_CallView_header_callType { .mx_CallView_header_callType {
font-size: 1.2rem;
font-weight: bold; font-weight: bold;
vertical-align: middle; vertical-align: middle;
} }
.mx_CallView_header_secondaryCallInfo {
&::before {
content: '·';
margin-left: 6px;
margin-right: 6px;
}
}
.mx_CallView_header_controls { .mx_CallView_header_controls {
margin-left: auto; margin-left: auto;
} }
@ -105,16 +227,31 @@ limitations under the License.
} }
} }
.mx_CallView_header_callInfo {
margin-left: 12px;
margin-right: 16px;
}
.mx_CallView_header_roomName { .mx_CallView_header_roomName {
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 12px;
line-height: initial; line-height: initial;
height: 15px;
}
.mx_CallView_secondaryCall_roomName {
margin-left: 4px;
} }
.mx_CallView_header_callTypeSmall { .mx_CallView_header_callTypeSmall {
font-size: 12px; font-size: 12px;
color: $secondary-fg-color; color: $secondary-fg-color;
line-height: initial; line-height: initial;
height: 15px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 240px;
} }
.mx_CallView_header_phoneIcon { .mx_CallView_header_phoneIcon {
@ -173,6 +310,18 @@ limitations under the License.
} }
} }
.mx_CallView_callControls_dialpad {
margin-right: auto;
&::before {
background-image: url('$(res)/img/voip/dialpad.svg');
}
}
.mx_CallView_callControls_button_dialpad_hidden {
margin-right: auto;
cursor: initial;
}
.mx_CallView_callControls_button_micOn { .mx_CallView_callControls_button_micOn {
&::before { &::before {
background-image: url('$(res)/img/voip/mic-on.svg'); background-image: url('$(res)/img/voip/mic-on.svg');
@ -203,6 +352,18 @@ limitations under the License.
} }
} }
.mx_CallView_callControls_button_more {
margin-left: auto;
&::before {
background-image: url('$(res)/img/voip/more.svg');
}
}
.mx_CallView_callControls_button_more_hidden {
margin-left: auto;
cursor: initial;
}
.mx_CallView_callControls_button_invisible { .mx_CallView_callControls_button_invisible {
visibility: hidden; visibility: hidden;
pointer-events: none; pointer-events: none;

View 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_DialPad {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.mx_DialPad_button {
width: 40px;
height: 40px;
background-color: $theme-button-bg-color;
border-radius: 40px;
font-size: 18px;
font-weight: 600;
text-align: center;
vertical-align: middle;
line-height: 40px;
}
.mx_DialPad_deleteButton, .mx_DialPad_dialButton {
&::before {
content: '';
display: inline-block;
height: 40px;
width: 40px;
vertical-align: middle;
mask-repeat: no-repeat;
mask-size: 20px;
mask-position: center;
background-color: $primary-bg-color;
}
}
.mx_DialPad_deleteButton {
background-color: $notice-primary-color;
&::before {
mask-image: url('$(res)/img/element-icons/call/delete.svg');
mask-position: 9px; // delete icon is right-heavy so have to be slightly to the left to look centered
}
}
.mx_DialPad_dialButton {
background-color: $accent-color;
&::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}

View file

@ -14,16 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as projectNameGenerator from "project-name-generator"; .mx_DialPadContextMenu_header {
margin-top: 12px;
/** margin-left: 12px;
* Generates a human readable identifier. This should not be used for anything margin-right: 12px;
* which needs secure/cryptographic random: just a level uniquness that is offered }
* by something like Date.now().
* @returns {string} The randomly generated ID .mx_DialPadContextMenu_title {
*/ color: $muted-fg-color;
export function generateHumanReadableId(): string { font-size: 12px;
return projectNameGenerator({words: 3}).raw.map(w => { font-weight: 600;
return w[0].toUpperCase() + w.substring(1).toLowerCase(); }
}).join('');
.mx_DialPadContextMenu_dialled {
height: 1em;
font-size: 18px;
font-weight: 600;
}
.mx_DialPadContextMenu_dialPad {
margin: 16px;
}
.mx_DialPadContextMenu_horizSep {
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
border-bottom: 1px solid $input-darker-bg-color;
}
} }

View file

@ -0,0 +1,74 @@
/*
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_Dialog_dialPadWrapper .mx_Dialog {
padding: 0px;
}
.mx_DialPadModal {
width: 192px;
height: 368px;
}
.mx_DialPadModal_header {
margin-top: 12px;
margin-left: 12px;
margin-right: 12px;
}
.mx_DialPadModal_title {
color: $muted-fg-color;
font-size: 12px;
font-weight: 600;
}
.mx_DialPadModal_cancel {
float: right;
mask: url('$(res)/img/feather-customised/cancel.svg');
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
width: 14px;
height: 14px;
background-color: $dialog-close-fg-color;
cursor: pointer;
}
.mx_DialPadModal_field {
border: none;
margin: 0px;
}
.mx_DialPadModal_field input {
font-size: 18px;
font-weight: 600;
}
.mx_DialPadModal_dialPad {
margin-left: 16px;
margin-right: 16px;
margin-top: 16px;
}
.mx_DialPadModal_horizSep {
position: relative;
&::before {
content: '';
position: absolute;
width: 100%;
border-bottom: 1px solid $input-darker-bg-color;
}
}

View file

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9803 1.2796C17.0771 2.54383 16.6773 3.79601 15.8657 4.77022C15.0784 5.74949 13.8854 6.31354 12.629 6.3006C12.5491 5.07276 12.9605 3.86352 13.7727 2.93921C14.5952 2.00238 15.7404 1.40982 16.9803 1.2796ZM20.9539 8.70795C19.5086 9.59652 18.6192 11.1635 18.5974 12.86C18.5994 14.7794 19.7489 16.5115 21.5166 17.2592C21.1766 18.3636 20.6642 19.4073 19.9982 20.3517C19.1038 21.6896 18.1661 22.9967 16.6777 23.0208C15.9698 23.0372 15.492 22.8336 14.9941 22.6215C14.4747 22.4003 13.9335 22.1697 13.0867 22.1697C12.1885 22.1697 11.6231 22.4077 11.0778 22.6372C10.6065 22.8355 10.1503 23.0275 9.50727 23.0542C8.08982 23.1067 7.00654 21.6263 6.07964 20.3009C4.22703 17.5943 2.78444 12.6733 4.71844 9.32483C5.62662 7.69286 7.32468 6.65727 9.19136 6.59696C9.99528 6.58042 10.7667 6.89028 11.443 7.16193C11.9602 7.36969 12.4219 7.5551 12.7999 7.5551C13.1321 7.5551 13.5809 7.37701 14.1038 7.16946C14.9276 6.84251 15.9356 6.44246 16.9628 6.55027C18.5589 6.60021 20.038 7.39984 20.9539 8.70795Z" fill="#17191C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,6 @@
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.12012 1.02C6.12012 0.45667 6.57679 0 7.14012 0C10.8957 0 13.9401 3.04446 13.9401 6.8C13.9401 7.36333 13.4834 7.82 12.9201 7.82C12.3568 7.82 11.9001 7.36333 11.9001 6.8C11.9001 4.17112 9.76899 2.04 7.14012 2.04C6.57679 2.04 6.12012 1.58333 6.12012 1.02Z" fill="#1E1E1E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.8799 15.98C10.8799 16.5433 10.4232 17 9.85988 17C6.10435 17 3.05989 13.9555 3.05989 10.2C3.05989 9.63667 3.51656 9.18 4.07989 9.18C4.64322 9.18 5.09989 9.63667 5.09989 10.2C5.09989 12.8289 7.23101 14.96 9.85988 14.96C10.4232 14.96 10.8799 15.4167 10.8799 15.98Z" fill="#1E1E1E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.02 10.88C0.45667 10.88 -1.99617e-08 10.4233 -4.45856e-08 9.86C-2.08745e-07 6.10447 3.04446 3.06 6.8 3.06C7.36333 3.06 7.82 3.51667 7.82 4.08C7.82 4.64334 7.36333 5.1 6.8 5.1C4.17113 5.1 2.04 7.23113 2.04 9.86C2.04 10.4233 1.58333 10.88 1.02 10.88Z" fill="#1E1E1E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.98 6.12C16.5433 6.12 17 6.57666 17 7.14C17 10.8955 13.9555 13.94 10.2 13.94C9.63667 13.94 9.18 13.4833 9.18 12.92C9.18 12.3567 9.63667 11.9 10.2 11.9C12.8289 11.9 14.96 9.76887 14.96 7.14C14.96 6.57666 15.4167 6.12 15.98 6.12Z" fill="#1E1E1E"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,9 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="2" y="1" width="22" height="22">
<path d="M2.10154 1.5H23.1003V22.3716H2.10154V1.5Z" fill="white"/>
</mask>
<g mask="url(#mask0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.1 11.999C23.1 6.20003 18.399 1.49902 12.6 1.49902C6.801 1.49902 2.1 6.20003 2.1 11.999C2.1 17.2399 5.9397 21.5838 10.9594 22.3715V15.0342H8.29336V11.999H10.9594V9.68574C10.9594 7.05418 12.5269 5.60059 14.9254 5.60059C16.0742 5.60059 17.2758 5.80566 17.2758 5.80566V8.38965H15.9518C14.6474 8.38965 14.2406 9.19903 14.2406 10.0294V11.999H17.1527L16.6872 15.0342H14.2406V22.3715C19.2603 21.5838 23.1 17.2399 23.1 11.999Z" fill="#1877F2"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6872 15.0342L17.1527 11.999H14.2406V10.0294C14.2406 9.19903 14.6474 8.38965 15.9518 8.38965H17.2758V5.80566C17.2758 5.80566 16.0742 5.60059 14.9254 5.60059C12.5269 5.60059 10.9594 7.05418 10.9594 9.68574V11.999H8.29336V15.0342H10.9594V22.3715C11.494 22.4553 12.0419 22.499 12.6 22.499C13.1581 22.499 13.706 22.4553 14.2406 22.3715V15.0342H16.6872Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8421 7.10595C19.9703 5.6121 18.7876 4.42942 17.2939 3.55764C15.8 2.68581 14.169 2.25001 12.3999 2.25001C10.6311 2.25001 8.9996 2.68594 7.50597 3.55764C6.01212 4.42938 4.82953 5.6121 3.95765 7.10595C3.08592 8.59976 2.65002 10.231 2.65002 11.9997C2.65002 14.1242 3.26987 16.0346 4.50987 17.7315C5.74973 19.4284 7.35145 20.6027 9.3149 21.2543C9.54345 21.2967 9.71264 21.2669 9.82265 21.1656C9.9327 21.0641 9.98766 20.937 9.98766 20.7848C9.98766 20.7595 9.98548 20.531 9.98126 20.0993C9.9769 19.6676 9.97485 19.291 9.97485 18.9696L9.68285 19.0202C9.49667 19.0543 9.26181 19.0687 8.97826 19.0646C8.69484 19.0607 8.40061 19.031 8.09598 18.9757C7.79121 18.921 7.50775 18.7941 7.24536 18.5952C6.98311 18.3963 6.79693 18.1359 6.68688 17.8145L6.55993 17.5224C6.47531 17.3279 6.3421 17.1119 6.1601 16.875C5.97811 16.638 5.79406 16.4773 5.60789 16.3927L5.519 16.329C5.45978 16.2868 5.40482 16.2358 5.35399 16.1766C5.30321 16.1174 5.2652 16.0582 5.23981 15.9988C5.21437 15.9395 5.23545 15.8908 5.30326 15.8526C5.37107 15.8144 5.49361 15.7959 5.67143 15.7959L5.92524 15.8338C6.09451 15.8677 6.3039 15.9691 6.55366 16.1384C6.80329 16.3077 7.0085 16.5277 7.16933 16.7984C7.36408 17.1455 7.59873 17.4099 7.87392 17.5919C8.14889 17.7739 8.42613 17.8648 8.70537 17.8648C8.98461 17.8648 9.22579 17.8436 9.429 17.8015C9.63198 17.7592 9.82243 17.6955 10.0002 17.611C10.0764 17.0437 10.2838 16.6079 10.6222 16.3033C10.1399 16.2526 9.70619 16.1762 9.32099 16.0747C8.93601 15.9731 8.53818 15.8081 8.12777 15.5794C7.71714 15.3509 7.37649 15.0673 7.10574 14.7289C6.83495 14.3904 6.61271 13.9459 6.43934 13.3959C6.26588 12.8457 6.17913 12.211 6.17913 11.4916C6.17913 10.4674 6.51351 9.59578 7.18213 8.87633C6.86892 8.10629 6.89849 7.24304 7.27093 6.28668C7.51638 6.21043 7.88037 6.26765 8.36273 6.45801C8.84517 6.64845 9.1984 6.8116 9.42277 6.94686C9.64714 7.08208 9.82692 7.19666 9.96236 7.2896C10.7496 7.06963 11.562 6.95962 12.3998 6.95962C13.2377 6.95962 14.0503 7.06963 14.8376 7.2896L15.32 6.98505C15.6498 6.78185 16.0394 6.59563 16.4877 6.42635C16.9363 6.25716 17.2793 6.21056 17.5164 6.28682C17.8971 7.24322 17.931 8.10642 17.6177 8.87647C18.2863 9.59591 18.6208 10.4677 18.6208 11.4918C18.6208 12.2111 18.5337 12.8478 18.3605 13.4023C18.1871 13.9568 17.963 14.4008 17.688 14.7353C17.4127 15.0697 17.0699 15.3511 16.6595 15.5795C16.249 15.808 15.851 15.973 15.466 16.0747C15.0809 16.1763 14.6472 16.2527 14.1648 16.3035C14.6048 16.6842 14.8248 17.2851 14.8248 18.106V20.7845C14.8248 20.9367 14.8777 21.0637 14.9836 21.1652C15.0894 21.2665 15.2565 21.2964 15.485 21.2539C17.4487 20.6024 19.0505 19.4281 20.2903 17.7311C21.53 16.0343 22.15 14.1238 22.15 11.9993C22.1496 10.2309 21.7135 8.59976 20.8421 7.10595Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -0,0 +1,9 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.0005 20.3296L15.3166 10.1293H8.68921L12.0005 20.3296Z" fill="#E24329"/>
<path d="M4.04348 10.1293L3.03364 13.2283C2.94226 13.5097 3.04095 13.8203 3.28214 13.9957L11.9996 20.3296L4.04348 10.1293Z" fill="#FCA326"/>
<path d="M4.04248 10.1289H8.68727L6.68828 3.98572C6.58597 3.67143 6.1401 3.67143 6.03411 3.98572L4.04248 10.1289Z" fill="#E24329"/>
<path d="M19.9602 10.1293L20.9664 13.2283C21.0577 13.5097 20.9591 13.8203 20.7179 13.9957L11.9991 20.3296L19.9602 10.1293Z" fill="#FCA326"/>
<path d="M19.9616 10.1289H15.3168L17.3121 3.98572C17.4144 3.67143 17.8603 3.67143 17.9663 3.98572L19.9616 10.1289Z" fill="#E24329"/>
<path d="M11.9991 20.3296L15.3153 10.1293H19.9601L11.9991 20.3296Z" fill="#FC6D26"/>
<path d="M11.9985 20.3296L4.04248 10.1293H8.68727L11.9985 20.3296Z" fill="#FC6D26"/>
</svg>

After

Width:  |  Height:  |  Size: 905 B

View file

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.501 12.2333C22.501 11.37 22.4296 10.74 22.2748 10.0867H12.2153V13.9833H18.12C18.001 14.9517 17.3582 16.41 15.9296 17.3899L15.9096 17.5204L19.0902 19.9351L19.3106 19.9567C21.3343 18.125 22.501 15.43 22.501 12.2333Z" fill="#4285F4"/>
<path d="M12.2147 22.5001C15.1075 22.5001 17.5361 21.5667 19.3099 19.9567L15.929 17.39C15.0242 18.0083 13.8099 18.44 12.2147 18.44C9.38142 18.44 6.97669 16.6083 6.11947 14.0767L5.99382 14.0871L2.68656 16.5955L2.64331 16.7133C4.40519 20.1433 8.02423 22.5001 12.2147 22.5001Z" fill="#34A853"/>
<path d="M6.12022 14.0767C5.89403 13.4234 5.76313 12.7233 5.76313 12C5.76313 11.2767 5.89403 10.5767 6.10832 9.92337L6.10233 9.78423L2.75361 7.2356L2.64405 7.28667C1.91789 8.71002 1.50122 10.3084 1.50122 12C1.50122 13.6917 1.91789 15.29 2.64405 16.7133L6.12022 14.0767Z" fill="#FBBC05"/>
<path d="M12.2148 5.55997C14.2267 5.55997 15.5838 6.41163 16.3576 7.12335L19.3814 4.23C17.5243 2.53834 15.1076 1.5 12.2148 1.5C8.02426 1.5 4.4052 3.85665 2.64331 7.28662L6.10759 9.92332C6.97671 7.39166 9.38146 5.55997 12.2148 5.55997Z" fill="#EB4335"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,3 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.04155 21C6.6153 21 4.35363 20.2943 2.45 19.0767C4.06624 19.1813 6.91855 18.9308 8.69268 17.2386C6.0238 17.1161 4.82019 15.0692 4.6632 14.1945C4.88997 14.2819 5.97147 14.3869 6.582 14.142C3.51192 13.3722 3.04094 10.678 3.1456 9.85573C3.72124 10.2581 4.69809 10.3981 5.08185 10.3631C2.22109 8.31618 3.25027 5.23707 3.75613 4.57226C5.80911 7.4165 8.8859 9.01393 12.6923 9.10278C12.6205 8.78802 12.5826 8.46032 12.5826 8.12373C12.5826 5.70819 14.5351 3.75 16.9435 3.75C18.2019 3.75 19.3358 4.28457 20.1318 5.13963C20.9727 4.94258 22.2382 4.4813 22.8569 4.0824C22.5451 5.20208 21.5742 6.13612 20.9869 6.48231C20.9918 6.49408 20.9821 6.47048 20.9869 6.48231C21.5028 6.40428 22.8986 6.13603 23.45 5.76192C23.1773 6.39094 22.148 7.4368 21.3033 8.02232C21.4604 14.9535 16.1574 21 9.04155 21Z" fill="#1D9BF0"/>
</svg>

After

Width:  |  Height:  |  Size: 916 B

View file

@ -0,0 +1,10 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path d="M18.3333 2.49951H5.83333C5.25833 2.49951 4.80833 2.79118 4.50833 3.23285L0 9.99951L4.50833 16.7578C4.80833 17.1995 5.25833 17.4995 5.83333 17.4995H18.3333C19.25 17.4995 20 16.7495 20 15.8328V4.16618C20 3.24951 19.25 2.49951 18.3333 2.49951ZM15.8333 12.9912L14.6583 14.1662L11.6667 11.1745L8.675 14.1662L7.5 12.9912L10.4917 9.99951L7.5 7.00784L8.675 5.83284L11.6667 8.82451L14.6583 5.83284L15.8333 7.00784L12.8417 9.99951L15.8333 12.9912Z" fill="white"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 692 B

View file

@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.99999 15.8335C9.08333 15.8335 8.33333 16.5835 8.33333 17.5002C8.33333 18.4168 9.08333 19.1668 9.99999 19.1668C10.9167 19.1668 11.6667 18.4168 11.6667 17.5002C11.6667 16.5835 10.9167 15.8335 9.99999 15.8335ZM4.99999 0.833496C4.08333 0.833496 3.33333 1.5835 3.33333 2.50016C3.33333 3.41683 4.08333 4.16683 4.99999 4.16683C5.91666 4.16683 6.66666 3.41683 6.66666 2.50016C6.66666 1.5835 5.91666 0.833496 4.99999 0.833496ZM4.99999 5.8335C4.08333 5.8335 3.33333 6.5835 3.33333 7.50016C3.33333 8.41683 4.08333 9.16683 4.99999 9.16683C5.91666 9.16683 6.66666 8.41683 6.66666 7.50016C6.66666 6.5835 5.91666 5.8335 4.99999 5.8335ZM4.99999 10.8335C4.08333 10.8335 3.33333 11.5835 3.33333 12.5002C3.33333 13.4168 4.08333 14.1668 4.99999 14.1668C5.91666 14.1668 6.66666 13.4168 6.66666 12.5002C6.66666 11.5835 5.91666 10.8335 4.99999 10.8335ZM15 4.16683C15.9167 4.16683 16.6667 3.41683 16.6667 2.50016C16.6667 1.5835 15.9167 0.833496 15 0.833496C14.0833 0.833496 13.3333 1.5835 13.3333 2.50016C13.3333 3.41683 14.0833 4.16683 15 4.16683ZM9.99999 10.8335C9.08333 10.8335 8.33333 11.5835 8.33333 12.5002C8.33333 13.4168 9.08333 14.1668 9.99999 14.1668C10.9167 14.1668 11.6667 13.4168 11.6667 12.5002C11.6667 11.5835 10.9167 10.8335 9.99999 10.8335ZM15 10.8335C14.0833 10.8335 13.3333 11.5835 13.3333 12.5002C13.3333 13.4168 14.0833 14.1668 15 14.1668C15.9167 14.1668 16.6667 13.4168 16.6667 12.5002C16.6667 11.5835 15.9167 10.8335 15 10.8335ZM15 5.8335C14.0833 5.8335 13.3333 6.5835 13.3333 7.50016C13.3333 8.41683 14.0833 9.16683 15 9.16683C15.9167 9.16683 16.6667 8.41683 16.6667 7.50016C16.6667 6.5835 15.9167 5.8335 15 5.8335ZM9.99999 5.8335C9.08333 5.8335 8.33333 6.5835 8.33333 7.50016C8.33333 8.41683 9.08333 9.16683 9.99999 9.16683C10.9167 9.16683 11.6667 8.41683 11.6667 7.50016C11.6667 6.5835 10.9167 5.8335 9.99999 5.8335ZM9.99999 0.833496C9.08333 0.833496 8.33333 1.5835 8.33333 2.50016C8.33333 3.41683 9.08333 4.16683 9.99999 4.16683C10.9167 4.16683 11.6667 3.41683 11.6667 2.50016C11.6667 1.5835 10.9167 0.833496 9.99999 0.833496Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

17
res/img/voip/dialpad.svg Normal file
View file

@ -0,0 +1,17 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="white"/>
</g>
<path d="M24 25.8335C23.0833 25.8335 22.3333 26.5835 22.3333 27.5002C22.3333 28.4168 23.0833 29.1668 24 29.1668C24.9167 29.1668 25.6667 28.4168 25.6667 27.5002C25.6667 26.5835 24.9167 25.8335 24 25.8335ZM19 10.8335C18.0833 10.8335 17.3333 11.5835 17.3333 12.5002C17.3333 13.4168 18.0833 14.1668 19 14.1668C19.9167 14.1668 20.6667 13.4168 20.6667 12.5002C20.6667 11.5835 19.9167 10.8335 19 10.8335ZM19 15.8335C18.0833 15.8335 17.3333 16.5835 17.3333 17.5002C17.3333 18.4168 18.0833 19.1668 19 19.1668C19.9167 19.1668 20.6667 18.4168 20.6667 17.5002C20.6667 16.5835 19.9167 15.8335 19 15.8335ZM19 20.8335C18.0833 20.8335 17.3333 21.5835 17.3333 22.5002C17.3333 23.4168 18.0833 24.1668 19 24.1668C19.9167 24.1668 20.6667 23.4168 20.6667 22.5002C20.6667 21.5835 19.9167 20.8335 19 20.8335ZM29 14.1668C29.9167 14.1668 30.6667 13.4168 30.6667 12.5002C30.6667 11.5835 29.9167 10.8335 29 10.8335C28.0833 10.8335 27.3333 11.5835 27.3333 12.5002C27.3333 13.4168 28.0833 14.1668 29 14.1668ZM24 20.8335C23.0833 20.8335 22.3333 21.5835 22.3333 22.5002C22.3333 23.4168 23.0833 24.1668 24 24.1668C24.9167 24.1668 25.6667 23.4168 25.6667 22.5002C25.6667 21.5835 24.9167 20.8335 24 20.8335ZM29 20.8335C28.0833 20.8335 27.3333 21.5835 27.3333 22.5002C27.3333 23.4168 28.0833 24.1668 29 24.1668C29.9167 24.1668 30.6667 23.4168 30.6667 22.5002C30.6667 21.5835 29.9167 20.8335 29 20.8335ZM29 15.8335C28.0833 15.8335 27.3333 16.5835 27.3333 17.5002C27.3333 18.4168 28.0833 19.1668 29 19.1668C29.9167 19.1668 30.6667 18.4168 30.6667 17.5002C30.6667 16.5835 29.9167 15.8335 29 15.8335ZM24 15.8335C23.0833 15.8335 22.3333 16.5835 22.3333 17.5002C22.3333 18.4168 23.0833 19.1668 24 19.1668C24.9167 19.1668 25.6667 18.4168 25.6667 17.5002C25.6667 16.5835 24.9167 15.8335 24 15.8335ZM24 10.8335C23.0833 10.8335 22.3333 11.5835 22.3333 12.5002C22.3333 13.4168 23.0833 14.1668 24 14.1668C24.9167 14.1668 25.6667 13.4168 25.6667 12.5002C25.6667 11.5835 24.9167 10.8335 24 10.8335Z" fill="#737D8C"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" 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="4"/>
<feGaussianBlur stdDeviation="2"/>
<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"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

17
res/img/voip/more.svg Normal file
View file

@ -0,0 +1,17 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M18.667 20C18.667 21.1046 17.7716 22 16.667 22C15.5624 22 14.667 21.1046 14.667 20C14.667 18.8954 15.5624 18 16.667 18C17.7716 18 18.667 18.8954 18.667 20ZM26 20C26 21.1046 25.1046 22 24 22C22.8954 22 22 21.1046 22 20C22 18.8954 22.8954 18 24 18C25.1046 18 26 18.8954 26 20ZM31.333 22C32.4376 22 33.333 21.1046 33.333 20C33.333 18.8954 32.4376 18 31.333 18C30.2284 18 29.333 18.8954 29.333 20C29.333 21.1046 30.2284 22 31.333 22Z" fill="#737D8C"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" 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="4"/>
<feGaussianBlur stdDeviation="2"/>
<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"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

3
res/img/voip/paused.svg Normal file
View 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 d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM11 16H9V8H11V16ZM15 16H13V8H15V16Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View file

@ -258,6 +258,12 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
// markdown overrides: // markdown overrides:
.mx_EventTile_content .markdown-body pre:hover { .mx_EventTile_content .markdown-body pre:hover {
border-color: #808080 !important; // inverted due to rules below border-color: #808080 !important; // inverted due to rules below
scrollbar-color: rgba(0, 0, 0, 0.2) transparent; // copied from light theme due to inversion below
// the code above works only in Firefox, this is for other browsers
// see https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color
&::-webkit-scrollbar-thumb {
background-color: rgba(0, 0, 0, 0.2); // copied from light theme due to inversion below
}
} }
.mx_EventTile_content .markdown-body { .mx_EventTile_content .markdown-body {
pre, code { pre, code {

View file

@ -237,7 +237,8 @@ $event-redacted-border-color: #cccccc;
$event-timestamp-color: #acacac; $event-timestamp-color: #acacac;
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; $copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
// e2e // e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color

View file

@ -237,6 +237,8 @@ $event-redacted-border-color: #cccccc;
$event-timestamp-color: #acacac; $event-timestamp-color: #acacac;
$copy-button-url: "$(res)/img/feather-customised/clipboard.svg"; $copy-button-url: "$(res)/img/feather-customised/clipboard.svg";
$collapse-button-url: "$(res)/img/feather-customised/minimise.svg";
$expand-button-url: "$(res)/img/feather-customised/maximise.svg";
// e2e // e2e
$e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color $e2e-verified-color: #76cfa5; // N.B. *NOT* the same as $accent-color

View file

@ -1,8 +1,7 @@
# Update on docker hub with the following commands in the directory of this file: # Update on docker hub with the following commands in the directory of this file:
# docker build -t matrixdotorg/riotweb-ci-e2etests-env:latest . # docker build -t vectorim/element-web-ci-e2etests-env:latest .
# docker log # docker push vectorim/element-web-ci-e2etests-env:latest
# docker push matrixdotorg/riotweb-ci-e2etests-env:latest FROM node:14-buster
FROM node:10
RUN apt-get update RUN apt-get update
RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
# dependencies for chrome (installed by puppeteer) # dependencies for chrome (installed by puppeteer)

View file

@ -2,11 +2,11 @@
# #
# script which is run by the CI build (after `yarn test`). # script which is run by the CI build (after `yarn test`).
# #
# clones riot-web develop and runs the tests against our version of react-sdk. # clones element-web develop and runs the tests against our version of react-sdk.
set -ev set -ev
scripts/ci/layered-riot-web.sh scripts/ci/layered.sh
cd ../riot-web cd element-web
yarn build:genfiles # so the tests can run. Faster version of `build` yarn build:genfiles # so the tests can run. Faster version of `build`
yarn test yarn test

View file

@ -2,7 +2,7 @@
# #
# script which is run by the CI build (after `yarn test`). # script which is run by the CI build (after `yarn test`).
# #
# clones riot-web develop and runs the tests against our version of react-sdk. # clones element-web develop and runs the tests against our version of react-sdk.
set -ev set -ev
@ -14,20 +14,20 @@ handle_error() {
trap 'handle_error' ERR trap 'handle_error' ERR
echo "--- Building Element" echo "--- Building Element"
scripts/ci/layered-riot-web.sh scripts/ci/layered.sh
cd ../riot-web cd element-web
riot_web_dir=`pwd` element_web_dir=`pwd`
CI_PACKAGE=true yarn build CI_PACKAGE=true yarn build
cd ../matrix-react-sdk cd ..
# run end to end tests # run end to end tests
pushd test/end-to-end-tests pushd test/end-to-end-tests
ln -s $riot_web_dir riot/riot-web ln -s $element_web_dir element/element-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
# CHROME_PATH=$(which google-chrome-stable) ./run.sh # CHROME_PATH=$(which google-chrome-stable) ./run.sh
echo "--- Install synapse & other dependencies" echo "--- Install synapse & other dependencies"
./install.sh ./install.sh
# install static webserver to server symlinked local copy of riot # install static webserver to server symlinked local copy of element
./riot/install-webserver.sh ./element/install-webserver.sh
rm -r logs || true rm -r logs || true
mkdir logs mkdir logs
echo "+++ Running end-to-end tests" echo "+++ Running end-to-end tests"

View file

@ -7,7 +7,6 @@ scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk pushd matrix-js-sdk
yarn link yarn link
yarn install $@ yarn install $@
yarn build
popd popd
yarn link matrix-js-sdk yarn link matrix-js-sdk

View file

@ -1,31 +0,0 @@
#!/bin/bash
# Creates an environment similar to one that riot-web would expect for
# development. This means going one directory up (and assuming we're in
# a directory like /workdir/matrix-react-sdk) and putting riot-web and
# the js-sdk there.
cd ../ # Assume we're at something like /workdir/matrix-react-sdk
# Set up the js-sdk first
matrix-react-sdk/scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk
yarn link
yarn install
popd
# Now set up the react-sdk
pushd matrix-react-sdk
yarn link matrix-js-sdk
yarn link
yarn install
popd
# Finally, set up riot-web
matrix-react-sdk/scripts/fetchdep.sh vector-im riot-web
pushd riot-web
yarn link matrix-js-sdk
yarn link matrix-react-sdk
yarn install
yarn build:res
popd

32
scripts/ci/layered.sh Executable file
View file

@ -0,0 +1,32 @@
#!/bin/bash
# Creates a layered environment with the full repo for the app and SDKs cloned
# and linked.
# Note that this style is different from the recommended developer setup: this
# file nests js-sdk and element-web inside react-sdk, while the local
# development setup places them all at the same level. We are nesting them here
# because some CI systems do not allow moving to a directory above the checkout
# for the primary repo (react-sdk in this case).
# Set up the js-sdk first
scripts/fetchdep.sh matrix-org matrix-js-sdk
pushd matrix-js-sdk
yarn link
yarn install
popd
# Now set up the react-sdk
yarn link matrix-js-sdk
yarn link
yarn install
yarn reskindex
# Finally, set up element-web
scripts/fetchdep.sh vector-im element-web
pushd element-web
yarn link matrix-js-sdk
yarn link matrix-react-sdk
yarn install
yarn build:res
popd

View file

@ -34,7 +34,7 @@ elif [[ "${#BUILDKITE_BRANCH_ARRAY[@]}" == "2" ]]; then
fi fi
# Try the target branch of the push or PR. # Try the target branch of the push or PR.
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
# Try the current branch from Jenkins. # Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
clone $deforg $defrepo `"echo $GIT_BRANCH" | sed -e 's/^origin\///'` clone $deforg $defrepo $HEAD
# Use the default branch as the last resort. # Use the default branch as the last resort.
clone $deforg $defrepo $defbranch clone $deforg $defrepo $defbranch

View file

@ -1,29 +1,30 @@
#!/usr/bin/env node #!/usr/bin/env node
var fs = require('fs'); const fs = require('fs');
var path = require('path'); const path = require('path');
var glob = require('glob'); const glob = require('glob');
var args = require('minimist')(process.argv); const util = require('util');
var chokidar = require('chokidar'); const args = require('minimist')(process.argv);
const chokidar = require('chokidar');
var componentIndex = path.join('src', 'component-index.js'); const componentIndex = path.join('src', 'component-index.js');
var componentIndexTmp = componentIndex+".tmp"; const componentIndexTmp = componentIndex+".tmp";
var componentsDir = path.join('src', 'components'); const componentsDir = path.join('src', 'components');
var componentJsGlob = '**/*.js'; const componentJsGlob = '**/*.js';
var componentTsGlob = '**/*.tsx'; const componentTsGlob = '**/*.tsx';
var prevFiles = []; let prevFiles = [];
function reskindex() { async function reskindex() {
var jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort(); const jsFiles = glob.sync(componentJsGlob, {cwd: componentsDir}).sort();
var tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort(); const tsFiles = glob.sync(componentTsGlob, {cwd: componentsDir}).sort();
var files = [...tsFiles, ...jsFiles]; const files = [...tsFiles, ...jsFiles];
if (!filesHaveChanged(files, prevFiles)) { if (!filesHaveChanged(files, prevFiles)) {
return; return;
} }
prevFiles = files; prevFiles = files;
var header = args.h || args.header; const header = args.h || args.header;
var strm = fs.createWriteStream(componentIndexTmp); const strm = fs.createWriteStream(componentIndexTmp);
if (header) { if (header) {
strm.write(fs.readFileSync(header)); strm.write(fs.readFileSync(header));
@ -38,11 +39,11 @@ function reskindex() {
strm.write(" */\n\n"); strm.write(" */\n\n");
strm.write("let components = {};\n"); strm.write("let components = {};\n");
for (var i = 0; i < files.length; ++i) { for (let i = 0; i < files.length; ++i) {
var file = files[i].replace('.js', '').replace('.tsx', ''); const file = files[i].replace('.js', '').replace('.tsx', '');
var moduleName = (file.replace(/\//g, '.')); const moduleName = (file.replace(/\//g, '.'));
var importName = moduleName.replace(/\./g, "$"); const importName = moduleName.replace(/\./g, "$");
strm.write("import " + importName + " from './components/" + file + "';\n"); strm.write("import " + importName + " from './components/" + file + "';\n");
strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");"); strm.write(importName + " && (components['"+moduleName+"'] = " + importName + ");");
@ -51,9 +52,10 @@ function reskindex() {
} }
strm.write("export {components};\n"); strm.write("export {components};\n");
strm.end(); // Ensure the file has been fully written to disk before proceeding
await util.promisify(strm.end);
fs.rename(componentIndexTmp, componentIndex, function(err) { fs.rename(componentIndexTmp, componentIndex, function(err) {
if(err) { if (err) {
console.error("Error moving new index into place: " + err); console.error("Error moving new index into place: " + err);
} else { } else {
console.log('Reskindex: completed'); console.log('Reskindex: completed');
@ -67,7 +69,7 @@ function filesHaveChanged(files, prevFiles) {
return true; return true;
} }
// Check for name changes // Check for name changes
for (var i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
if (prevFiles[i] !== files[i]) { if (prevFiles[i] !== files[i]) {
return true; return true;
} }
@ -81,7 +83,7 @@ if (!args.w) {
return; return;
} }
var watchDebouncer = null; let watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
if (path === componentIndex) return; if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer); if (watchDebouncer) clearTimeout(watchDebouncer);

View file

@ -36,6 +36,8 @@ import {Analytics} from "../Analytics";
import CountlyAnalytics from "../CountlyAnalytics"; import CountlyAnalytics from "../CountlyAnalytics";
import UserActivity from "../UserActivity"; import UserActivity from "../UserActivity";
import {ModalWidgetStore} from "../stores/ModalWidgetStore"; import {ModalWidgetStore} from "../stores/ModalWidgetStore";
import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
declare global { declare global {
interface Window { interface Window {
@ -59,11 +61,13 @@ declare global {
mxNotifier: typeof Notifier; mxNotifier: typeof Notifier;
mxRightPanelStore: RightPanelStore; mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore; mxWidgetStore: WidgetStore;
mxWidgetLayoutStore: WidgetLayoutStore;
mxCallHandler: CallHandler; mxCallHandler: CallHandler;
mxAnalytics: Analytics; mxAnalytics: Analytics;
mxCountlyAnalytics: typeof CountlyAnalytics; mxCountlyAnalytics: typeof CountlyAnalytics;
mxUserActivity: UserActivity; mxUserActivity: UserActivity;
mxModalWidgetStore: ModalWidgetStore; mxModalWidgetStore: ModalWidgetStore;
mxVoipUserMapper: VoipUserMapper;
} }
interface Document { interface Document {

View file

@ -18,6 +18,7 @@ limitations under the License.
*/ */
import {MatrixClient} from "matrix-js-sdk/src/client"; import {MatrixClient} from "matrix-js-sdk/src/client";
import {encodeUnpaddedBase64} from "matrix-js-sdk/src/crypto/olmlib";
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import BaseEventIndexManager from './indexing/BaseEventIndexManager'; import BaseEventIndexManager from './indexing/BaseEventIndexManager';
import {ActionPayload} from "./dispatcher/payloads"; import {ActionPayload} from "./dispatcher/payloads";
@ -25,9 +26,11 @@ import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
import {Action} from "./dispatcher/actions"; import {Action} from "./dispatcher/actions";
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast"; import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import {idbLoad, idbSave, idbDelete} from "./utils/StorageManager";
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url"; export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url"; export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
export const SSO_IDP_ID_KEY = "mx_sso_idp_id";
export enum UpdateCheckStatus { export enum UpdateCheckStatus {
Checking = "CHECKING", Checking = "CHECKING",
@ -54,7 +57,7 @@ export default abstract class BasePlatform {
this.startUpdateCheck = this.startUpdateCheck.bind(this); this.startUpdateCheck = this.startUpdateCheck.bind(this);
} }
abstract async getConfig(): Promise<{}>; abstract getConfig(): Promise<{}>;
abstract getDefaultDeviceDisplayName(): string; abstract getDefaultDeviceDisplayName(): string;
@ -270,6 +273,9 @@ export default abstract class BasePlatform {
if (mxClient.getIdentityServerUrl()) { if (mxClient.getIdentityServerUrl()) {
localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl()); localStorage.setItem(SSO_ID_SERVER_URL_KEY, mxClient.getIdentityServerUrl());
} }
if (idpId) {
localStorage.setItem(SSO_IDP_ID_KEY, idpId);
}
const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin); const callbackUrl = this.getSSOCallbackUrl(fragmentAfterLogin);
window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO window.location.href = mxClient.getSsoLoginUrl(callbackUrl.toString(), loginType, idpId); // redirect to SSO
} }
@ -287,8 +293,41 @@ export default abstract class BasePlatform {
* pickle key has been stored. * pickle key has been stored.
*/ */
async getPickleKey(userId: string, deviceId: string): Promise<string | null> { async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
if (!window.crypto || !window.crypto.subtle) {
return null; return null;
} }
let data;
try {
data = await idbLoad("pickleKey", [userId, deviceId]);
} catch (e) {}
if (!data) {
return null;
}
if (!data.encrypted || !data.iv || !data.cryptoKey) {
console.error("Badly formatted pickle key");
return null;
}
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
for (let i = 0; i < userId.length; i++) {
additionalData[i] = userId.charCodeAt(i);
}
additionalData[userId.length] = 124; // "|"
for (let i = 0; i < deviceId.length; i++) {
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
}
try {
const key = await crypto.subtle.decrypt(
{name: "AES-GCM", iv: data.iv, additionalData}, data.cryptoKey,
data.encrypted,
);
return encodeUnpaddedBase64(key);
} catch (e) {
console.error("Error decrypting pickle key");
return null;
}
}
/** /**
* Create and store a pickle key for encrypting libolm objects. * Create and store a pickle key for encrypting libolm objects.
@ -298,8 +337,38 @@ export default abstract class BasePlatform {
* support storing pickle keys. * support storing pickle keys.
*/ */
async createPickleKey(userId: string, deviceId: string): Promise<string | null> { async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
if (!window.crypto || !window.crypto.subtle) {
return null; return null;
} }
const crypto = window.crypto;
const randomArray = new Uint8Array(32);
crypto.getRandomValues(randomArray);
const cryptoKey = await crypto.subtle.generateKey(
{name: "AES-GCM", length: 256}, false, ["encrypt", "decrypt"],
);
const iv = new Uint8Array(32);
crypto.getRandomValues(iv);
const additionalData = new Uint8Array(userId.length + deviceId.length + 1);
for (let i = 0; i < userId.length; i++) {
additionalData[i] = userId.charCodeAt(i);
}
additionalData[userId.length] = 124; // "|"
for (let i = 0; i < deviceId.length; i++) {
additionalData[userId.length + 1 + i] = deviceId.charCodeAt(i);
}
const encrypted = await crypto.subtle.encrypt(
{name: "AES-GCM", iv, additionalData}, cryptoKey, randomArray,
);
try {
await idbSave("pickleKey", [userId, deviceId], {encrypted, iv, cryptoKey});
} catch (e) {
return null;
}
return encodeUnpaddedBase64(randomArray);
}
/** /**
* Delete a previously stored pickle key from storage. * Delete a previously stored pickle key from storage.
@ -307,5 +376,8 @@ export default abstract class BasePlatform {
* @param {string} userId the device ID that the pickle key is for. * @param {string} userId the device ID that the pickle key is for.
*/ */
async destroyPickleKey(userId: string, deviceId: string): Promise<void> { async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
try {
await idbDelete("pickleKey", [userId, deviceId]);
} catch (e) {}
} }
} }

View file

@ -64,7 +64,6 @@ import dis from './dispatcher/dispatcher';
import WidgetUtils from './utils/WidgetUtils'; import WidgetUtils from './utils/WidgetUtils';
import WidgetEchoStore from './stores/WidgetEchoStore'; import WidgetEchoStore from './stores/WidgetEchoStore';
import SettingsStore from './settings/SettingsStore'; import SettingsStore from './settings/SettingsStore';
import {generateHumanReadableId} from "./utils/NamingUtils";
import {Jitsi} from "./widgets/Jitsi"; import {Jitsi} from "./widgets/Jitsi";
import {WidgetType} from "./widgets/WidgetType"; import {WidgetType} from "./widgets/WidgetType";
import {SettingLevel} from "./settings/SettingLevel"; import {SettingLevel} from "./settings/SettingLevel";
@ -81,6 +80,22 @@ import Analytics from './Analytics';
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import {UIFeature} from "./settings/UIFeature"; import {UIFeature} from "./settings/UIFeature";
import { CallError } from "matrix-js-sdk/src/webrtc/call"; import { CallError } from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
import { randomString } from "matrix-js-sdk/src/randomstring";
export const PROTOCOL_PSTN = 'm.protocol.pstn';
export const PROTOCOL_PSTN_PREFIXED = 'im.vector.protocol.pstn';
export const PROTOCOL_SIP_NATIVE = 'im.vector.protocol.sip_native';
export const PROTOCOL_SIP_VIRTUAL = 'im.vector.protocol.sip_virtual';
const CHECK_PROTOCOLS_ATTEMPTS = 3;
// Event type for room account data and room creation content used to mark rooms as virtual rooms
// (and store the ID of their native room)
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
enum AudioID { enum AudioID {
Ring = 'ringAudio', Ring = 'ringAudio',
@ -89,6 +104,29 @@ enum AudioID {
Busy = 'busyAudio', Busy = 'busyAudio',
} }
interface ThirdpartyLookupResponseFields {
/* eslint-disable camelcase */
// im.vector.sip_native
virtual_mxid?: string;
is_virtual?: boolean;
// im.vector.sip_virtual
native_mxid?: string;
is_native?: boolean;
// common
lookup_success?: boolean;
/* eslint-enable camelcase */
}
interface ThirdpartyLookupResponse {
userid: string,
protocol: string,
fields: ThirdpartyLookupResponseFields,
}
// Unlike 'CallType' in js-sdk, this one includes screen sharing // Unlike 'CallType' in js-sdk, this one includes screen sharing
// (because a screen sharing call is only a screen sharing call to the caller, // (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 // to the callee it's just a video call, at least as far as the current impl
@ -115,8 +153,16 @@ function getRemoteAudioElement(): HTMLAudioElement {
} }
export default class CallHandler { export default class CallHandler {
private calls = new Map<string, MatrixCall>(); private calls = new Map<string, MatrixCall>(); // roomId -> call
private audioPromises = new Map<AudioID, Promise<void>>(); private audioPromises = new Map<AudioID, Promise<void>>();
private dispatcherRef: string = null;
private supportsPstnProtocol = null;
private pstnSupportPrefixed = null; // True if the server only support the prefixed pstn protocol
private supportsSipNativeVirtual = null; // im.vector.protocol.sip_virtual and im.vector.protocol.sip_native
private pstnSupportCheckTimer: NodeJS.Timeout; // number actually because we're in the browser
// For rooms we've been invited to, true if they're from virtual user, false if we've checked and they aren't.
private invitedRoomsAreVirtual = new Map<string, boolean>();
private invitedRoomCheckInProgress = false;
static sharedInstance() { static sharedInstance() {
if (!window.mxCallHandler) { if (!window.mxCallHandler) {
@ -126,8 +172,17 @@ export default class CallHandler {
return window.mxCallHandler; return window.mxCallHandler;
} }
/*
* Gets the user-facing room associated with a call (call.roomId may be the call "virtual room"
* if a voip_mxid_translate_pattern is set in the config)
*/
public static roomIdForCall(call: MatrixCall): string {
if (!call) return null;
return VoipUserMapper.sharedInstance().nativeRoomForVirtualRoom(call.roomId) || call.roomId;
}
start() { start() {
dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
// add empty handlers for media actions, otherwise the media keys // add empty handlers for media actions, otherwise the media keys
// end up causing the audio elements with our ring/ringback etc // end up causing the audio elements with our ring/ringback etc
// audio clips in to play. // audio clips in to play.
@ -143,6 +198,8 @@ export default class CallHandler {
if (SettingsStore.getValue(UIFeature.Voip)) { if (SettingsStore.getValue(UIFeature.Voip)) {
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming); MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
} }
this.checkProtocols(CHECK_PROTOCOLS_ATTEMPTS);
} }
stop() { stop() {
@ -150,6 +207,77 @@ export default class CallHandler {
if (cli) { if (cli) {
cli.removeListener('Call.incoming', this.onCallIncoming); cli.removeListener('Call.incoming', this.onCallIncoming);
} }
if (this.dispatcherRef !== null) {
dis.unregister(this.dispatcherRef);
this.dispatcherRef = null;
}
}
private async checkProtocols(maxTries) {
try {
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
if (protocols[PROTOCOL_PSTN] !== undefined) {
this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN]);
if (this.supportsPstnProtocol) this.pstnSupportPrefixed = false;
} else if (protocols[PROTOCOL_PSTN_PREFIXED] !== undefined) {
this.supportsPstnProtocol = Boolean(protocols[PROTOCOL_PSTN_PREFIXED]);
if (this.supportsPstnProtocol) this.pstnSupportPrefixed = true;
} else {
this.supportsPstnProtocol = null;
}
dis.dispatch({action: Action.PstnSupportUpdated});
if (protocols[PROTOCOL_SIP_NATIVE] !== undefined && protocols[PROTOCOL_SIP_VIRTUAL] !== undefined) {
this.supportsSipNativeVirtual = Boolean(
protocols[PROTOCOL_SIP_NATIVE] && protocols[PROTOCOL_SIP_VIRTUAL],
);
}
dis.dispatch({action: Action.VirtualRoomSupportUpdated});
} catch (e) {
if (maxTries === 1) {
console.log("Failed to check for protocol support and no retries remain: assuming no support", e);
} else {
console.log("Failed to check for protocol support: will retry", e);
this.pstnSupportCheckTimer = setTimeout(() => {
this.checkProtocols(maxTries - 1);
}, 10000);
}
}
}
public getSupportsPstnProtocol() {
return this.supportsPstnProtocol;
}
public getSupportsVirtualRooms() {
return this.supportsPstnProtocol;
}
public pstnLookup(phoneNumber: string): Promise<ThirdpartyLookupResponse[]> {
return MatrixClientPeg.get().getThirdpartyUser(
this.pstnSupportPrefixed ? PROTOCOL_PSTN_PREFIXED : PROTOCOL_PSTN, {
'm.id.phone': phoneNumber,
},
);
}
public sipVirtualLookup(nativeMxid: string): Promise<ThirdpartyLookupResponse[]> {
return MatrixClientPeg.get().getThirdpartyUser(
PROTOCOL_SIP_VIRTUAL, {
'native_mxid': nativeMxid,
},
);
}
public sipNativeLookup(virtualMxid: string): Promise<ThirdpartyLookupResponse[]> {
return MatrixClientPeg.get().getThirdpartyUser(
PROTOCOL_SIP_NATIVE, {
'virtual_mxid': virtualMxid,
},
);
} }
private onCallIncoming = (call) => { private onCallIncoming = (call) => {
@ -175,6 +303,28 @@ export default class CallHandler {
return null; return null;
} }
getAllActiveCalls() {
const activeCalls = [];
for (const call of this.calls.values()) {
if (call.state !== CallState.Ended && call.state !== CallState.Ringing) {
activeCalls.push(call);
}
}
return activeCalls;
}
getAllActiveCallsNotInRoom(notInThisRoomId) {
const callsNotInThatRoom = [];
for (const [roomId, call] of this.calls.entries()) {
if (roomId !== notInThisRoomId && call.state !== CallState.Ended) {
callsNotInThatRoom.push(call);
}
}
return callsNotInThatRoom;
}
play(audioId: AudioID) { play(audioId: AudioID) {
// TODO: Attach an invisible element for this instead // TODO: Attach an invisible element for this instead
// which listens? // which listens?
@ -222,11 +372,15 @@ export default class CallHandler {
// We don't allow placing more than one call per room, but that doesn't mean there // 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 // 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. // is the call we consider 'the' call for its room.
const callForThisRoom = this.getCallForRoom(call.roomId); const mappedRoomId = CallHandler.roomIdForCall(call);
const callForThisRoom = this.getCallForRoom(mappedRoomId);
return callForThisRoom && call.callId === callForThisRoom.callId; return callForThisRoom && call.callId === callForThisRoom.callId;
} }
private setCallListeners(call: MatrixCall) { private setCallListeners(call: MatrixCall) {
const mappedRoomId = CallHandler.roomIdForCall(call);
call.on(CallEvent.Error, (err: CallError) => { call.on(CallEvent.Error, (err: CallError) => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
@ -256,7 +410,7 @@ export default class CallHandler {
Analytics.trackEvent('voip', 'callHangup'); Analytics.trackEvent('voip', 'callHangup');
this.removeCallForRoom(call.roomId); this.removeCallForRoom(mappedRoomId);
}); });
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => { call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
if (!this.matchesCallForThisRoom(call)) return; if (!this.matchesCallForThisRoom(call)) return;
@ -280,8 +434,9 @@ export default class CallHandler {
this.play(AudioID.Ringback); this.play(AudioID.Ringback);
break; break;
case CallState.Ended: case CallState.Ended:
{
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason); Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
this.removeCallForRoom(call.roomId); this.removeCallForRoom(mappedRoomId);
if (oldState === CallState.InviteSent && ( if (oldState === CallState.InviteSent && (
call.hangupParty === CallParty.Remote || call.hangupParty === CallParty.Remote ||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout) (call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
@ -313,9 +468,14 @@ export default class CallHandler {
title: _t("Answered Elsewhere"), title: _t("Answered Elsewhere"),
description: _t("The call was answered on another device."), description: _t("The call was answered on another device."),
}); });
} else { } else if (oldState !== CallState.Fledgling && oldState !== CallState.Ringing) {
// don't play the end-call sound for calls that never got off the ground
this.play(AudioID.CallEnd); this.play(AudioID.CallEnd);
} }
this.logCallStats(call, mappedRoomId);
break;
}
} }
}); });
call.on(CallEvent.Replaced, (newCall: MatrixCall) => { call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
@ -329,25 +489,70 @@ export default class CallHandler {
this.pause(AudioID.Ringback); this.pause(AudioID.Ringback);
} }
this.calls.set(newCall.roomId, newCall); this.calls.set(mappedRoomId, newCall);
this.setCallListeners(newCall); this.setCallListeners(newCall);
this.setCallState(newCall, newCall.state); this.setCallState(newCall, newCall.state);
}); });
} }
private async logCallStats(call: MatrixCall, mappedRoomId: string) {
const stats = await call.getCurrentCallStats();
logger.debug(
`Call completed. Call ID: ${call.callId}, virtual room ID: ${call.roomId}, ` +
`user-facing room ID: ${mappedRoomId}, direction: ${call.direction}, ` +
`our Party ID: ${call.ourPartyId}, hangup party: ${call.hangupParty}, ` +
`hangup reason: ${call.hangupReason}`,
);
if (!stats) {
logger.debug(
"Call statistics are undefined. The call has " +
"probably failed before a peerConn was established",
);
return;
}
logger.debug("Local candidates:");
for (const cand of stats.filter(item => item.type === 'local-candidate')) {
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
logger.debug(
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
`protocol: ${cand.protocol}, relay protocol: ${cand.relayProtocol}, network type: ${cand.networkType}`,
);
}
logger.debug("Remote candidates:");
for (const cand of stats.filter(item => item.type === 'remote-candidate')) {
const address = cand.address || cand.ip; // firefox uses 'address', chrome uses 'ip'
logger.debug(
`${cand.id} - type: ${cand.candidateType}, address: ${address}, port: ${cand.port}, ` +
`protocol: ${cand.protocol}`,
);
}
logger.debug("Candidate pairs:");
for (const pair of stats.filter(item => item.type === 'candidate-pair')) {
logger.debug(
`${pair.localCandidateId} / ${pair.remoteCandidateId} - state: ${pair.state}, ` +
`nominated: ${pair.nominated}, ` +
`requests sent ${pair.requestsSent}, requests received ${pair.requestsReceived}, ` +
`responses received: ${pair.responsesReceived}, responses sent: ${pair.responsesSent}, ` +
`bytes received: ${pair.bytesReceived}, bytes sent: ${pair.bytesSent}, `,
);
}
}
private setCallAudioElement(call: MatrixCall) { private setCallAudioElement(call: MatrixCall) {
const audioElement = getRemoteAudioElement(); const audioElement = getRemoteAudioElement();
if (audioElement) call.setRemoteAudioElement(audioElement); if (audioElement) call.setRemoteAudioElement(audioElement);
} }
private setCallState(call: MatrixCall, status: CallState) { private setCallState(call: MatrixCall, status: CallState) {
const mappedRoomId = CallHandler.roomIdForCall(call);
console.log( console.log(
`Call state in ${call.roomId} changed to ${status}`, `Call state in ${mappedRoomId} changed to ${status}`,
); );
dis.dispatch({ dis.dispatch({
action: 'call_state', action: 'call_state',
room_id: call.roomId, room_id: mappedRoomId,
state: status, state: status,
}); });
} }
@ -393,14 +598,14 @@ export default class CallHandler {
title = _t("Unable to access microphone"); title = _t("Unable to access microphone");
description = <div> description = <div>
{_t( {_t(
"Call failed because no microphone could not be accessed. " + "Call failed because microphone could not be accessed. " +
"Check that a microphone is plugged in and set up correctly.", "Check that a microphone is plugged in and set up correctly.",
)} )}
</div>; </div>;
} else if (call.type === CallType.Video) { } else if (call.type === CallType.Video) {
title = _t("Unable to access webcam / microphone"); title = _t("Unable to access webcam / microphone");
description = <div> description = <div>
{_t("Call failed because no webcam or microphone could not be accessed. Check that:")} {_t("Call failed because webcam or microphone could not be accessed. Check that:")}
<ul> <ul>
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li> <li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
<li>{_t("Permission is granted to use the webcam")}</li> <li>{_t("Permission is granted to use the webcam")}</li>
@ -414,17 +619,25 @@ export default class CallHandler {
}, null, true); }, null, true);
} }
private placeCall( private async placeCall(
roomId: string, type: PlaceCallType, roomId: string, type: PlaceCallType,
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement, localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
) { ) {
Analytics.trackEvent('voip', 'placeCall', 'type', type); Analytics.trackEvent('voip', 'placeCall', 'type', type);
CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
const mappedRoomId = (await VoipUserMapper.sharedInstance().getOrCreateVirtualRoomForRoom(roomId)) || roomId;
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);
this.setCallListeners(call); this.setCallListeners(call);
this.setCallAudioElement(call); this.setCallAudioElement(call);
this.setActiveCallRoomId(roomId);
if (type === PlaceCallType.Voice) { if (type === PlaceCallType.Voice) {
call.placeVoiceCall(); call.placeVoiceCall();
} else if (type === 'video') { } else if (type === 'video') {
@ -443,9 +656,17 @@ export default class CallHandler {
}); });
return; return;
} }
call.placeScreenSharingCall(remoteElement, localElement);
call.placeScreenSharingCall(
remoteElement,
localElement,
async () : Promise<DesktopCapturerSource> => {
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
});
} else { } else {
console.error("Unknown conf call type: %s", type); console.error("Unknown conf call type: " + type);
} }
} }
@ -453,12 +674,10 @@ export default class CallHandler {
switch (payload.action) { switch (payload.action) {
case 'place_call': case 'place_call':
{ {
if (this.getAnyActiveCall()) { // We might be using managed hybrid widgets
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, { if (isManagedHybridWidgetEnabled()) {
title: _t('Existing Call'), addManagedHybridWidget(payload.room_id);
description: _t('You are already in a call.'), return;
});
return; // don't allow >1 call to be placed.
} }
// if the runtime env doesn't do VoIP, whine. // if the runtime env doesn't do VoIP, whine.
@ -470,9 +689,18 @@ export default class CallHandler {
return; return;
} }
// don't allow > 2 calls to be placed.
if (this.getAllActiveCalls().length > 1) {
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
title: _t('Too Many Calls'),
description: _t("You've reached the maximum number of simultaneous calls."),
});
return;
}
const room = MatrixClientPeg.get().getRoom(payload.room_id); const room = MatrixClientPeg.get().getRoom(payload.room_id);
if (!room) { if (!room) {
console.error("Room %s does not exist.", payload.room_id); console.error(`Room ${payload.room_id} does not exist.`);
return; return;
} }
@ -483,7 +711,7 @@ export default class CallHandler {
}); });
return; return;
} else if (members.length === 2) { } else if (members.length === 2) {
console.info("Place %s call in %s", payload.type, payload.room_id); console.info(`Place ${payload.type} call in ${payload.room_id}`);
this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element); this.placeCall(payload.room_id, payload.type, payload.local_element, payload.remote_element);
} else { // > 2 } else { // > 2
@ -498,39 +726,43 @@ export default class CallHandler {
} }
break; break;
case 'place_conference_call': case 'place_conference_call':
console.info("Place conference call in %s", payload.room_id); console.info("Place conference call in " + payload.room_id);
Analytics.trackEvent('voip', 'placeConferenceCall'); Analytics.trackEvent('voip', 'placeConferenceCall');
CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true); CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true);
this.startCallApp(payload.room_id, payload.type); this.startCallApp(payload.room_id, payload.type);
break; break;
case 'end_conference': case 'end_conference':
console.info("Terminating conference call in %s", payload.room_id); console.info("Terminating conference call in " + payload.room_id);
this.terminateCallApp(payload.room_id); this.terminateCallApp(payload.room_id);
break; break;
case 'hangup_conference': case 'hangup_conference':
console.info("Leaving conference call in %s", payload.room_id); console.info("Leaving conference call in "+ payload.room_id);
this.hangupCallApp(payload.room_id); this.hangupCallApp(payload.room_id);
break; break;
case 'incoming_call': 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 the runtime env doesn't do VoIP, stop here.
if (!MatrixClientPeg.get().supportsVoip()) { if (!MatrixClientPeg.get().supportsVoip()) {
return; return;
} }
const call = payload.call as MatrixCall; const call = payload.call as MatrixCall;
const mappedRoomId = CallHandler.roomIdForCall(call);
if (this.getCallForRoom(mappedRoomId)) {
// ignore multiple incoming calls to the same room
return;
}
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.calls.set(call.roomId, call) this.calls.set(mappedRoomId, call)
this.setCallListeners(call); this.setCallListeners(call);
this.setCallAudioElement(call);
// get ready to send encrypted events in the room, so if the user does answer
// the call, we'll be ready to send. NB. This is the protocol-level room ID not
// the mapped one: that's where we'll send the events.
const cli = MatrixClientPeg.get();
cli.prepareToEncrypt(cli.getRoom(call.roomId));
} }
break; break;
case 'hangup': case 'hangup':
@ -543,14 +775,26 @@ export default class CallHandler {
} else { } else {
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false); this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
} }
this.removeCallForRoom(payload.room_id); // don't remove the call yet: let the hangup event handler do it (otherwise it will throw
// the hangup event away)
break; break;
case 'answer': { case 'answer': {
if (!this.calls.has(payload.room_id)) { if (!this.calls.has(payload.room_id)) {
return; // no call to answer return; // no call to answer
} }
if (this.getAllActiveCalls().length > 1) {
Modal.createTrackedDialog('Call Handler', 'Existing Call', ErrorDialog, {
title: _t('Too Many Calls'),
description: _t("You've reached the maximum number of simultaneous calls."),
});
return;
}
const call = this.calls.get(payload.room_id); const call = this.calls.get(payload.room_id);
call.answer(); call.answer();
this.setCallAudioElement(call);
this.setActiveCallRoomId(payload.room_id);
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false); CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
dis.dispatch({ dis.dispatch({
action: "view_room", action: "view_room",
@ -561,6 +805,33 @@ export default class CallHandler {
} }
} }
setActiveCallRoomId(activeCallRoomId: string) {
logger.info("Setting call in room " + activeCallRoomId + " active");
for (const [roomId, call] of this.calls.entries()) {
if (call.state === CallState.Ended) continue;
if (roomId === activeCallRoomId) {
call.setRemoteOnHold(false);
} else {
logger.info("Holding call in room " + roomId + " because another call is being set active");
call.setRemoteOnHold(true);
}
}
}
/**
* @returns true if we are currently in any call where we haven't put the remote party on hold
*/
hasAnyUnheldCall() {
for (const call of this.calls.values()) {
if (call.state === CallState.Ended) continue;
if (!call.isRemoteOnHold()) return true;
}
return false;
}
private async startCallApp(roomId: string, type: string) { private async startCallApp(roomId: string, type: string) {
dis.dispatch({ dis.dispatch({
action: 'appsDrawer', action: 'appsDrawer',
@ -591,7 +862,7 @@ export default class CallHandler {
confId = base32.stringify(Buffer.from(roomId), { pad: false }); confId = base32.stringify(Buffer.from(roomId), { pad: false });
} else { } else {
// Create a random human readable conference ID // Create a random human readable conference ID
confId = `JitsiConference${generateHumanReadableId()}`; confId = `JitsiConference${randomString(32)}`;
} }
let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth}); let widgetUrl = WidgetUtils.getLocalJitsiWrapperUrl({auth: jitsiAuth});
@ -607,6 +878,7 @@ export default class CallHandler {
isAudioOnly: type === 'voice', isAudioOnly: type === 'voice',
domain: jitsiDomain, domain: jitsiDomain,
auth: jitsiAuth, auth: jitsiAuth,
roomName: room.name,
}; };
const widgetId = ( const widgetId = (

View file

@ -497,7 +497,7 @@ export default class ContentMessages {
content.info.mimetype = file.type; content.info.mimetype = file.type;
} }
const prom = new Promise((resolve) => { const prom = new Promise<void>((resolve) => {
if (file.type.indexOf('image/') === 0) { if (file.type.indexOf('image/') === 0) {
content.msgtype = 'm.image'; content.msgtype = 'm.image';
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => { infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {

View file

@ -840,7 +840,7 @@ export default class CountlyAnalytics {
let endTime = CountlyAnalytics.getTimestamp(); let endTime = CountlyAnalytics.getTimestamp();
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli.getRoom(roomId)) { if (!cli.getRoom(roomId)) {
await new Promise(resolve => { await new Promise<void>(resolve => {
const handler = (room) => { const handler = (room) => {
if (room.roomId === roomId) { if (room.roomId === roomId) {
cli.off("Room", handler); cli.off("Room", handler);
@ -880,7 +880,7 @@ export default class CountlyAnalytics {
let endTime = CountlyAnalytics.getTimestamp(); let endTime = CountlyAnalytics.getTimestamp();
if (!room.findEventById(eventId)) { if (!room.findEventById(eventId)) {
await new Promise(resolve => { await new Promise<void>(resolve => {
const handler = (ev) => { const handler = (ev) => {
if (ev.getId() === eventId) { if (ev.getId() === eventId) {
room.off("Room.localEchoUpdated", handler); room.off("Room.localEchoUpdated", handler);

View file

@ -163,7 +163,7 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
attribs.target = '_blank'; // by default attribs.target = '_blank'; // by default
const transformed = tryTransformPermalinkToLocalHref(attribs.href); const transformed = tryTransformPermalinkToLocalHref(attribs.href);
if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.VECTOR_URL_PATTERN)) { if (transformed !== attribs.href || attribs.href.match(linkifyMatrix.ELEMENT_URL_PATTERN)) {
attribs.href = transformed; attribs.href = transformed;
delete attribs.target; delete attribs.target;
} }
@ -422,6 +422,8 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
if (SettingsStore.getValue("feature_latex_maths")) { if (SettingsStore.getValue("feature_latex_maths")) {
const phtml = cheerio.load(safeBody, const phtml = cheerio.load(safeBody,
{ _useHtmlParser2: true, decodeEntities: false }) { _useHtmlParser2: true, decodeEntities: false })
// @ts-ignore - The types for `replaceWith` wrongly expect
// Cheerio instance to be returned.
phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) { phtml('div, span[data-mx-maths!=""]').replaceWith(function(i, e) {
return katex.renderToString( return katex.renderToString(
AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')), AllHtmlEntities.decode(phtml(e).attr('data-mx-maths')),
@ -438,13 +440,14 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
delete sanitizeParams.textFilter; delete sanitizeParams.textFilter;
} }
const contentBody = isDisplayedWithHtml ? safeBody : strippedBody;
if (opts.returnString) { if (opts.returnString) {
return isDisplayedWithHtml ? safeBody : strippedBody; return contentBody;
} }
let emojiBody = false; let emojiBody = false;
if (!opts.disableBigEmoji && bodyHasEmoji) { if (!opts.disableBigEmoji && bodyHasEmoji) {
let contentBodyTrimmed = strippedBody !== undefined ? strippedBody.trim() : ''; let contentBodyTrimmed = contentBody !== undefined ? contentBody.trim() : '';
// Ignore spaces in body text. Emojis with spaces in between should // Ignore spaces in body text. Emojis with spaces in between should
// still be counted as purely emoji messages. // still be counted as purely emoji messages.

View file

@ -165,6 +165,7 @@ export default class IdentityAuthClient {
}); });
const [confirmed] = await finished; const [confirmed] = await finished;
if (confirmed) { if (confirmed) {
// eslint-disable-next-line react-hooks/rules-of-hooks
useDefaultIdentityServer(); useDefaultIdentityServer();
} else { } else {
throw new AbortedIdentityActionError( throw new AbortedIdentityActionError(

View file

@ -21,6 +21,7 @@ limitations under the License.
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import { InvalidStoreError } from "matrix-js-sdk/src/errors"; import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg'; import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security"; import SecurityCustomisations from "./customisations/Security";
@ -45,11 +46,13 @@ import {IntegrationManagers} from "./integrations/IntegrationManagers";
import {Mjolnir} from "./mjolnir/Mjolnir"; import {Mjolnir} from "./mjolnir/Mjolnir";
import DeviceListener from "./DeviceListener"; import DeviceListener from "./DeviceListener";
import {Jitsi} from "./widgets/Jitsi"; import {Jitsi} from "./widgets/Jitsi";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY} from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import CallHandler from './CallHandler'; import CallHandler from './CallHandler';
import LifecycleCustomisations from "./customisations/Lifecycle"; import LifecycleCustomisations from "./customisations/Lifecycle";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import {_t} from "./languageHandler";
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url"; const ID_SERVER_URL_KEY = "mx_is_url";
@ -147,20 +150,13 @@ export async function loadSession(opts: ILoadSessionOpts = {}): Promise<boolean>
* Gets the user ID of the persisted session, if one exists. This does not validate * Gets the user ID of the persisted session, if one exists. This does not validate
* that the user's credentials still work, just that they exist and that a user ID * that the user's credentials still work, just that they exist and that a user ID
* is associated with them. The session is not loaded. * is associated with them. The session is not loaded.
* @returns {String} The persisted session's owner, if an owner exists. Null otherwise. * @returns {[String, bool]} The persisted session's owner and whether the stored
* session is for a guest user, if an owner exists. If there is no stored session,
* return [null, null].
*/ */
export function getStoredSessionOwner(): string { export async function getStoredSessionOwner(): Promise<[string, boolean]> {
const {hsUrl, userId, accessToken} = getLocalStorageSessionVars(); const {hsUrl, userId, hasAccessToken, isGuest} = await getStoredSessionVars();
return hsUrl && userId && accessToken ? userId : null; return hsUrl && userId && hasAccessToken ? [userId, isGuest] : [null, null];
}
/**
* @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(): boolean {
const sessVars = getLocalStorageSessionVars();
return sessVars.hsUrl && sessVars.userId && sessVars.accessToken ? sessVars.isGuest : null;
} }
/** /**
@ -168,7 +164,8 @@ export function getStoredSessionIsGuest(): boolean {
* query-parameters extracted from the real query-string of the starting * query-parameters extracted from the real query-string of the starting
* URI. * URI.
* *
* @param {String} defaultDeviceDisplayName * @param {string} defaultDeviceDisplayName
* @param {string} fragmentAfterLogin path to go to after a successful login, only used for "Try again"
* *
* @returns {Promise} promise which resolves to true if we completed the token * @returns {Promise} promise which resolves to true if we completed the token
* login, else false * login, else false
@ -176,6 +173,7 @@ export function getStoredSessionIsGuest(): boolean {
export function attemptTokenLogin( export function attemptTokenLogin(
queryParams: Record<string, string>, queryParams: Record<string, string>,
defaultDeviceDisplayName?: string, defaultDeviceDisplayName?: string,
fragmentAfterLogin?: string,
): Promise<boolean> { ): Promise<boolean> {
if (!queryParams.loginToken) { if (!queryParams.loginToken) {
return Promise.resolve(false); return Promise.resolve(false);
@ -185,6 +183,12 @@ export function attemptTokenLogin(
const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY); const identityServer = localStorage.getItem(SSO_ID_SERVER_URL_KEY);
if (!homeserver) { if (!homeserver) {
console.warn("Cannot log in with token: can't determine HS URL to use"); console.warn("Cannot log in with token: can't determine HS URL to use");
Modal.createTrackedDialog("SSO", "Unknown HS", ErrorDialog, {
title: _t("We couldn't log you in"),
description: _t("We asked the browser to remember which homeserver you use to let you sign in, " +
"but unfortunately your browser has forgotten it. Go to the sign in page and try again."),
button: _t("Try again"),
});
return Promise.resolve(false); return Promise.resolve(false);
} }
@ -197,15 +201,35 @@ export function attemptTokenLogin(
}, },
).then(function(creds) { ).then(function(creds) {
console.log("Logged in with token"); console.log("Logged in with token");
return clearStorage().then(() => { return clearStorage().then(async () => {
persistCredentialsToLocalStorage(creds); await persistCredentials(creds);
// remember that we just logged in // remember that we just logged in
sessionStorage.setItem("mx_fresh_login", String(true)); sessionStorage.setItem("mx_fresh_login", String(true));
return true; return true;
}); });
}).catch((err) => { }).catch((err) => {
console.error("Failed to log in with login token: " + err + " " + Modal.createTrackedDialog("SSO", "Token Rejected", ErrorDialog, {
err.data); title: _t("We couldn't log you in"),
description: err.name === "ConnectionError"
? _t("Your homeserver was unreachable and was not able to log you in. Please try again. " +
"If this continues, please contact your homeserver administrator.")
: _t("Your homeserver rejected your log in attempt. " +
"This could be due to things just taking too long. Please try again. " +
"If this continues, please contact your homeserver administrator."),
button: _t("Try again"),
onFinished: tryAgain => {
if (tryAgain) {
const cli = Matrix.createClient({
baseUrl: homeserver,
idBaseUrl: identityServer,
});
const idpId = localStorage.getItem(SSO_IDP_ID_KEY) || undefined;
PlatformPeg.get().startSingleSignOn(cli, "sso", fragmentAfterLogin, idpId);
}
},
});
console.error("Failed to log in with login token:");
console.error(err);
return false; return false;
}); });
} }
@ -276,24 +300,42 @@ function registerAsGuest(
}); });
} }
export interface ILocalStorageSession { export interface IStoredSession {
hsUrl: string; hsUrl: string;
isUrl: string; isUrl: string;
accessToken: string; hasAccessToken: boolean;
accessToken: string | object;
userId: string; userId: string;
deviceId: string; deviceId: string;
isGuest: boolean; isGuest: boolean;
} }
/** /**
* Retrieves information about the stored session in localstorage. The session * Retrieves information about the stored session from the browser's storage. The session
* may not be valid, as it is not tested for consistency here. * may not be valid, as it is not tested for consistency here.
* @returns {Object} Information about the session - see implementation for variables. * @returns {Object} Information about the session - see implementation for variables.
*/ */
export function getLocalStorageSessionVars(): ILocalStorageSession { export async function getStoredSessionVars(): Promise<IStoredSession> {
const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY); const hsUrl = localStorage.getItem(HOMESERVER_URL_KEY);
const isUrl = localStorage.getItem(ID_SERVER_URL_KEY); const isUrl = localStorage.getItem(ID_SERVER_URL_KEY);
const accessToken = localStorage.getItem("mx_access_token"); let accessToken;
try {
accessToken = await StorageManager.idbLoad("account", "mx_access_token");
} catch (e) {}
if (!accessToken) {
accessToken = localStorage.getItem("mx_access_token");
if (accessToken) {
try {
// try to migrate access token to IndexedDB if we can
await StorageManager.idbSave("account", "mx_access_token", accessToken);
localStorage.removeItem("mx_access_token");
} catch (e) {}
}
}
// if we pre-date storing "mx_has_access_token", but we retrieved an access
// token, then we should say we have an access token
const hasAccessToken =
(localStorage.getItem("mx_has_access_token") === "true") || !!accessToken;
const userId = localStorage.getItem("mx_user_id"); const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id"); const deviceId = localStorage.getItem("mx_device_id");
@ -305,7 +347,43 @@ export function getLocalStorageSessionVars(): ILocalStorageSession {
isGuest = localStorage.getItem("matrix-is-guest") === "true"; isGuest = localStorage.getItem("matrix-is-guest") === "true";
} }
return {hsUrl, isUrl, accessToken, userId, deviceId, isGuest}; return {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest};
}
// The pickle key is a string of unspecified length and format. For AES, we
// need a 256-bit Uint8Array. So we HKDF the pickle key to generate the AES
// key. The AES key should be zeroed after it is used.
async function pickleKeyToAesKey(pickleKey: string): Promise<Uint8Array> {
const pickleKeyBuffer = new Uint8Array(pickleKey.length);
for (let i = 0; i < pickleKey.length; i++) {
pickleKeyBuffer[i] = pickleKey.charCodeAt(i);
}
const hkdfKey = await window.crypto.subtle.importKey(
"raw", pickleKeyBuffer, "HKDF", false, ["deriveBits"],
);
pickleKeyBuffer.fill(0);
return new Uint8Array(await window.crypto.subtle.deriveBits(
{
name: "HKDF", hash: "SHA-256",
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/879
salt: new Uint8Array(32), info: new Uint8Array(0),
},
hkdfKey,
256,
));
}
async function abortLogin() {
const signOut = await showStorageEvictedDialog();
if (signOut) {
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(
"Aborting login in progress because of storage inconsistency",
);
}
} }
// returns a promise which resolves to true if a session is found in // returns a promise which resolves to true if a session is found in
@ -318,14 +396,18 @@ export function getLocalStorageSessionVars(): ILocalStorageSession {
// The plan is to gradually move the localStorage access done here into // The plan is to gradually move the localStorage access done here into
// SessionStore to avoid bugs where the view becomes out-of-sync with // SessionStore to avoid bugs where the view becomes out-of-sync with
// localStorage (e.g. isGuest etc.) // localStorage (e.g. isGuest etc.)
async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> { export async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promise<boolean> {
const ignoreGuest = opts?.ignoreGuest; const ignoreGuest = opts?.ignoreGuest;
if (!localStorage) { if (!localStorage) {
return false; return false;
} }
const {hsUrl, isUrl, accessToken, userId, deviceId, isGuest} = getLocalStorageSessionVars(); const {hsUrl, isUrl, hasAccessToken, accessToken, userId, deviceId, isGuest} = await getStoredSessionVars();
if (hasAccessToken && !accessToken) {
abortLogin();
}
if (accessToken && userId && hsUrl) { if (accessToken && userId && hsUrl) {
if (ignoreGuest && isGuest) { if (ignoreGuest && isGuest) {
@ -333,9 +415,15 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
return false; return false;
} }
let decryptedAccessToken = accessToken;
const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId); const pickleKey = await PlatformPeg.get().getPickleKey(userId, deviceId);
if (pickleKey) { if (pickleKey) {
console.log("Got pickle key"); console.log("Got pickle key");
if (typeof accessToken !== "string") {
const encrKey = await pickleKeyToAesKey(pickleKey);
decryptedAccessToken = await decryptAES(accessToken, encrKey, "access_token");
encrKey.fill(0);
}
} else { } else {
console.log("No pickle key available"); console.log("No pickle key available");
} }
@ -347,7 +435,7 @@ async function restoreFromLocalStorage(opts?: { ignoreGuest?: boolean }): Promis
await doSetLoggedIn({ await doSetLoggedIn({
userId: userId, userId: userId,
deviceId: deviceId, deviceId: deviceId,
accessToken: accessToken, accessToken: decryptedAccessToken as string,
homeserverUrl: hsUrl, homeserverUrl: hsUrl,
identityServerUrl: isUrl, identityServerUrl: isUrl,
guest: isGuest, guest: isGuest,
@ -486,15 +574,7 @@ async function doSetLoggedIn(
// crypto store, we'll be generally confused when handling encrypted data. // crypto store, we'll be generally confused when handling encrypted data.
// Show a modal recommending a full reset of storage. // Show a modal recommending a full reset of storage.
if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) { if (results.dataInLocalStorage && results.cryptoInited && !results.dataInCryptoStore) {
const signOut = await showStorageEvictedDialog(); await abortLogin();
if (signOut) {
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(
"Aborting login in progress because of storage inconsistency",
);
}
} }
Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl); Analytics.setLoggedIn(credentials.guest, credentials.homeserverUrl);
@ -516,7 +596,7 @@ async function doSetLoggedIn(
if (localStorage) { if (localStorage) {
try { try {
persistCredentialsToLocalStorage(credentials); await persistCredentials(credentials);
// make sure we don't think that it's a fresh login any more // make sure we don't think that it's a fresh login any more
sessionStorage.removeItem("mx_fresh_login"); sessionStorage.removeItem("mx_fresh_login");
} catch (e) { } catch (e) {
@ -545,18 +625,55 @@ function showStorageEvictedDialog(): Promise<boolean> {
// `instanceof`. Babel 7 supports this natively in their class handling. // `instanceof`. Babel 7 supports this natively in their class handling.
class AbortLoginAndRebuildStorage extends Error { } class AbortLoginAndRebuildStorage extends Error { }
function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void { async function persistCredentials(credentials: IMatrixClientCreds): Promise<void> {
localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl); localStorage.setItem(HOMESERVER_URL_KEY, credentials.homeserverUrl);
if (credentials.identityServerUrl) { if (credentials.identityServerUrl) {
localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl); localStorage.setItem(ID_SERVER_URL_KEY, credentials.identityServerUrl);
} }
localStorage.setItem("mx_user_id", credentials.userId); localStorage.setItem("mx_user_id", credentials.userId);
localStorage.setItem("mx_access_token", credentials.accessToken);
localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest)); localStorage.setItem("mx_is_guest", JSON.stringify(credentials.guest));
// store whether we expect to find an access token, to detect the case
// where IndexedDB is blown away
if (credentials.accessToken) {
localStorage.setItem("mx_has_access_token", "true");
} else {
localStorage.deleteItem("mx_has_access_token");
}
if (credentials.pickleKey) { if (credentials.pickleKey) {
let encryptedAccessToken;
try {
// try to encrypt the access token using the pickle key
const encrKey = await pickleKeyToAesKey(credentials.pickleKey);
encryptedAccessToken = await encryptAES(credentials.accessToken, encrKey, "access_token");
encrKey.fill(0);
} catch (e) {
console.warn("Could not encrypt access token", e);
}
try {
// save either the encrypted access token, or the plain access
// token if we were unable to encrypt (e.g. if the browser doesn't
// have WebCrypto).
await StorageManager.idbSave(
"account", "mx_access_token",
encryptedAccessToken || credentials.accessToken,
);
} catch (e) {
// if we couldn't save to indexedDB, fall back to localStorage. We
// store the access token unencrypted since localStorage only saves
// strings.
localStorage.setItem("mx_access_token", credentials.accessToken);
}
localStorage.setItem("mx_has_pickle_key", String(true)); localStorage.setItem("mx_has_pickle_key", String(true));
} else { } else {
try {
await StorageManager.idbSave(
"account", "mx_access_token", credentials.accessToken,
);
} catch (e) {
localStorage.setItem("mx_access_token", credentials.accessToken);
}
if (localStorage.getItem("mx_has_pickle_key")) { if (localStorage.getItem("mx_has_pickle_key")) {
console.error("Expected a pickle key, but none provided. Encryption may not work."); console.error("Expected a pickle key, but none provided. Encryption may not work.");
} }
@ -733,6 +850,10 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
window.localStorage.clear(); window.localStorage.clear();
try {
await StorageManager.idbDelete("account", "mx_access_token");
} catch (e) {}
// now restore those invites // now restore those invites
if (!opts?.deleteEverything) { if (!opts?.deleteEverything) {
pendingInvites.forEach(i => { pendingInvites.forEach(i => {

View file

@ -33,16 +33,24 @@ interface IPasswordFlow {
type: "m.login.password"; type: "m.login.password";
} }
export enum IdentityProviderBrand {
Gitlab = "org.matrix.gitlab",
Github = "org.matrix.github",
Apple = "org.matrix.apple",
Google = "org.matrix.google",
Facebook = "org.matrix.facebook",
Twitter = "org.matrix.twitter",
}
export interface IIdentityProvider { export interface IIdentityProvider {
id: string; id: string;
name: string; name: string;
icon?: string; icon?: string;
brand?: IdentityProviderBrand | string;
} }
export interface ISSOFlow { export interface ISSOFlow {
type: "m.login.sso" | "m.login.cas"; type: "m.login.sso" | "m.login.cas";
// eslint-disable-next-line camelcase
identity_providers: IIdentityProvider[];
"org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858 "org.matrix.msc2858.identity_providers": IIdentityProvider[]; // Unstable prefix for MSC2858
} }

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import commonmark from 'commonmark'; import * as commonmark from 'commonmark';
import {escape} from "lodash"; import {escape} from "lodash";
const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u']; const ALLOWED_HTML_TAGS = ['sub', 'sup', 'del', 'u'];

View file

@ -279,6 +279,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
timelineSupport: true, timelineSupport: true,
forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'), forceTURN: !SettingsStore.getValue('webRtcAllowPeerToPeer'),
fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'), fallbackICEServerAllowed: !!SettingsStore.getValue('fallbackICEServerAllowed'),
// Gather up to 20 ICE candidates when a call arrives: this should be more than we'd
// ever normally need, so effectively this should make all the gathering happen when
// the call arrives.
iceCandidatePoolSize: 20,
verificationMethods: [ verificationMethods: [
verificationMethods.SAS, verificationMethods.SAS,
SHOW_QR_CODE_METHOD, SHOW_QR_CODE_METHOD,

View file

@ -202,12 +202,13 @@ function setRoomNotifsStateUnmuted(roomId, newState) {
} }
function findOverrideMuteRule(roomId) { function findOverrideMuteRule(roomId) {
if (!MatrixClientPeg.get().pushRules || const cli = MatrixClientPeg.get();
!MatrixClientPeg.get().pushRules['global'] || if (!cli.pushRules ||
!MatrixClientPeg.get().pushRules['global'].override) { !cli.pushRules['global'] ||
!cli.pushRules['global'].override) {
return null; return null;
} }
for (const rule of MatrixClientPeg.get().pushRules['global'].override) { for (const rule of cli.pushRules['global'].override) {
if (isRuleForRoom(roomId, rule)) { if (isRuleForRoom(roomId, rule)) {
if (isMuteRule(rule) && rule.enabled) { if (isMuteRule(rule) && rule.enabled) {
return rule; return rule;

View file

@ -46,7 +46,9 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
import SdkConfig from "./SdkConfig"; import SdkConfig from "./SdkConfig";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {UIFeature} from "./settings/UIFeature"; import {UIFeature} from "./settings/UIFeature";
import {CHAT_EFFECTS} from "./effects"
import CallHandler from "./CallHandler"; import CallHandler from "./CallHandler";
import {guessAndSetDMRoom} from "./Rooms";
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816 // XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
interface HTMLInputEvent extends Event { interface HTMLInputEvent extends Event {
@ -78,6 +80,7 @@ export const CommandCategories = {
"actions": _td("Actions"), "actions": _td("Actions"),
"admin": _td("Admin"), "admin": _td("Admin"),
"advanced": _td("Advanced"), "advanced": _td("Advanced"),
"effects": _td("Effects"),
"other": _td("Other"), "other": _td("Other"),
}; };
@ -164,6 +167,32 @@ export const Commands = [
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
new Command({
command: 'tableflip',
args: '<message>',
description: _td('Prepends (╯°□°)╯︵ ┻━┻ to a plain-text message'),
runFn: function(roomId, args) {
let message = '(╯°□°)╯︵ ┻━┻';
if (args) {
message = message + ' ' + args;
}
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
},
category: CommandCategories.messages,
}),
new Command({
command: 'unflip',
args: '<message>',
description: _td('Prepends ┬──┬ ( ゜-゜ノ) to a plain-text message'),
runFn: function(roomId, args) {
let message = '┬──┬ ( ゜-゜ノ)';
if (args) {
message = message + ' ' + args;
}
return success(MatrixClientPeg.get().sendTextMessage(roomId, message));
},
category: CommandCategories.messages,
}),
new Command({ new Command({
command: 'lenny', command: 'lenny',
args: '<message>', args: '<message>',
@ -1011,9 +1040,7 @@ export const Commands = [
return success((async () => { return success((async () => {
if (isPhoneNumber) { if (isPhoneNumber) {
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', { const results = await CallHandler.sharedInstance().pstnLookup(this.state.value);
'm.id.phone': userId,
});
if (!results || results.length === 0 || !results[0].userid) { if (!results || results.length === 0 || !results[0].userid) {
throw new Error("Unable to find Matrix ID for phone number"); throw new Error("Unable to find Matrix ID for phone number");
} }
@ -1084,6 +1111,24 @@ export const Commands = [
return success(); return success();
}, },
}), }),
new Command({
command: "converttodm",
description: _td("Converts the room to a DM"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const room = MatrixClientPeg.get().getRoom(roomId);
return success(guessAndSetDMRoom(room, true));
},
}),
new Command({
command: "converttoroom",
description: _td("Converts the DM to a room"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const room = MatrixClientPeg.get().getRoom(roomId);
return success(guessAndSetDMRoom(room, false));
},
}),
// Command definitions for autocompletion ONLY: // Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes // /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
@ -1094,6 +1139,30 @@ export const Commands = [
category: CommandCategories.messages, category: CommandCategories.messages,
hideCompletionAfterSpace: true, hideCompletionAfterSpace: true,
}), }),
...CHAT_EFFECTS.map((effect) => {
return new Command({
command: effect.command,
description: effect.description(),
args: '<message>',
runFn: function(roomId, args) {
return success((async () => {
if (!args) {
args = effect.fallbackMessage();
MatrixClientPeg.get().sendEmoteMessage(roomId, args);
} else {
const content = {
msgtype: effect.msgType,
body: args,
};
MatrixClientPeg.get().sendMessage(roomId, content);
}
dis.dispatch({action: `effects.${effect.command}`});
})());
},
category: CommandCategories.effects,
})
}),
]; ];
// build a map from names and aliases to the Command objects. // build a map from names and aliases to the Command objects.
@ -1111,7 +1180,7 @@ export function parseCommandString(input: string) {
input = input.replace(/\s+$/, ''); input = input.replace(/\s+$/, '');
if (input[0] !== '/') return {}; // not a command if (input[0] !== '/') return {}; // not a command
const bits = input.match(/^(\S+?)(?: +((.|\n)*))?$/); const bits = input.match(/^(\S+?)(?:[ \n]+((.|\n)*))?$/);
let cmd; let cmd;
let args; let args;
if (bits) { if (bits) {

View file

@ -19,6 +19,7 @@ import * as Roles from './Roles';
import {isValid3pidInvite} from "./RoomInvite"; import {isValid3pidInvite} from "./RoomInvite";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList"; import {ALL_RULE_TYPES, ROOM_RULE_TYPES, SERVER_RULE_TYPES, USER_RULE_TYPES} from "./mjolnir/BanList";
import {WIDGET_LAYOUT_EVENT_TYPE} from "./stores/widgets/WidgetLayoutStore";
function textForMemberEvent(ev) { function textForMemberEvent(ev) {
// XXX: SYJS-16 "sender is sometimes null for join messages" // XXX: SYJS-16 "sender is sometimes null for join messages"
@ -477,6 +478,11 @@ function textForWidgetEvent(event) {
} }
} }
function textForWidgetLayoutEvent(event) {
const senderName = event.sender?.name || event.getSender();
return _t("%(senderName)s has updated the widget layout", {senderName});
}
function textForMjolnirEvent(event) { function textForMjolnirEvent(event) {
const senderName = event.getSender(); const senderName = event.getSender();
const {entity: prevEntity} = event.getPrevContent(); const {entity: prevEntity} = event.getPrevContent();
@ -583,6 +589,7 @@ const stateHandlers = {
// TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111)
'im.vector.modular.widgets': textForWidgetEvent, 'im.vector.modular.widgets': textForWidgetEvent,
[WIDGET_LAYOUT_EVENT_TYPE]: textForWidgetLayoutEvent,
}; };
// Add all the Mjolnir stuff to the renderer // Add all the Mjolnir stuff to the renderer

110
src/VoipUserMapper.ts Normal file
View file

@ -0,0 +1,110 @@
/*
Copyright 2021 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 { ensureVirtualRoomExists, findDMForUser } from './createRoom';
import { MatrixClientPeg } from "./MatrixClientPeg";
import DMRoomMap from "./utils/DMRoomMap";
import CallHandler, { VIRTUAL_ROOM_EVENT_TYPE } from './CallHandler';
import { Room } from 'matrix-js-sdk/src/models/room';
// Functions for mapping virtual users & rooms. Currently the only lookup
// is sip virtual: there could be others in the future.
export default class VoipUserMapper {
private virtualRoomIdCache = new Set<string>();
public static sharedInstance(): VoipUserMapper {
if (window.mxVoipUserMapper === undefined) window.mxVoipUserMapper = new VoipUserMapper();
return window.mxVoipUserMapper;
}
private async userToVirtualUser(userId: string): Promise<string> {
const results = await CallHandler.sharedInstance().sipVirtualLookup(userId);
if (results.length === 0) return null;
return results[0].userid;
}
public async getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> {
const userId = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (!userId) return null;
const virtualUser = await this.userToVirtualUser(userId);
if (!virtualUser) return null;
const virtualRoomId = await ensureVirtualRoomExists(MatrixClientPeg.get(), virtualUser, roomId);
MatrixClientPeg.get().setRoomAccountData(virtualRoomId, VIRTUAL_ROOM_EVENT_TYPE, {
native_room: roomId,
});
return virtualRoomId;
}
public nativeRoomForVirtualRoom(roomId: string):string {
const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
if (!virtualRoom) return null;
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
if (!virtualRoomEvent || !virtualRoomEvent.getContent()) return null;
return virtualRoomEvent.getContent()['native_room'] || null;
}
public isVirtualRoom(room: Room):boolean {
if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
if (this.virtualRoomIdCache.has(room.roomId)) return true;
// also look in the create event for the claimed native room ID, which is the only
// way we can recognise a virtual room we've created when it first arrives down
// our stream. We don't trust this in general though, as it could be faked by an
// inviter: our main source of truth is the DM state.
const roomCreateEvent = room.currentState.getStateEvents("m.room.create", "");
if (!roomCreateEvent || !roomCreateEvent.getContent()) return false;
// we only look at this for rooms we created (so inviters can't just cause rooms
// to be invisible)
if (roomCreateEvent.getSender() !== MatrixClientPeg.get().getUserId()) return false;
const claimedNativeRoomId = roomCreateEvent.getContent()[VIRTUAL_ROOM_EVENT_TYPE];
return Boolean(claimedNativeRoomId);
}
public async onNewInvitedRoom(invitedRoom: Room) {
const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);
if (result.length === 0) {
return true;
}
if (result[0].fields.is_virtual) {
const nativeUser = result[0].userid;
const nativeRoom = findDMForUser(MatrixClientPeg.get(), nativeUser);
if (nativeRoom) {
// It's a virtual room with a matching native room, so set the room account data. This
// will make sure we know where how to map calls and also allow us know not to display
// it in the future.
MatrixClientPeg.get().setRoomAccountData(invitedRoom.roomId, VIRTUAL_ROOM_EVENT_TYPE, {
native_room: nativeRoom.roomId,
});
// also auto-join the virtual room if we have a matching native room
// (possibly we should only join if we've also joined the native room, then we'd also have
// to make sure we joined virtual rooms on joining a native one)
MatrixClientPeg.get().joinRoom(invitedRoom.roomId);
}
// also put this room in the virtual room ID cache so isVirtualRoom return the right answer
// in however long it takes for the echo of setAccountData to come down the sync
this.virtualRoomIdCache.add(invitedRoom.roomId);
}
}
}

View file

@ -168,6 +168,12 @@ const shortcuts: Record<Categories, IShortcut[]> = {
key: Key.U, key: Key.U,
}], }],
description: _td("Upload a file"), description: _td("Upload a file"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: Key.F,
}],
description: _td("Search (must be enabled)"),
}, },
], ],

View file

@ -95,7 +95,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const blob = new Blob([this._keyBackupInfo.recovery_key], { const blob = new Blob([this._keyBackupInfo.recovery_key], {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'recovery-key.txt'); FileSaver.saveAs(blob, 'security-key.txt');
this.setState({ this.setState({
downloaded: true, downloaded: true,
@ -238,7 +238,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
)}</p> )}</p>
<p>{_t( <p>{_t(
"We'll store an encrypted copy of your keys on our server. " + "We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a recovery passphrase.", "Secure your backup with a Security Phrase.",
)}</p> )}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p> <p>{_t("For maximum security, this should be different from your account password.")}</p>
@ -252,10 +252,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onValidate={this._onPassPhraseValidate} onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField} fieldRef={this._passphraseField}
autoFocus={true} autoFocus={true}
label={_td("Enter a recovery passphrase")} label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a recovery passphrase")} labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")} labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")} labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/> />
</div> </div>
</div> </div>
@ -270,7 +270,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
<details> <details>
<summary>{_t("Advanced")}</summary> <summary>{_t("Advanced")}</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} > <AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a recovery key")} {_t("Set up with a Security Key")}
</AccessibleButton> </AccessibleButton>
</details> </details>
</form>; </form>;
@ -310,7 +310,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}> return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t( <p>{_t(
"Please enter your recovery passphrase a second time to confirm.", "Please enter your Security Phrase a second time to confirm.",
)}</p> )}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer"> <div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -319,7 +319,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
onChange={this._onPassPhraseConfirmChange} onChange={this._onPassPhraseConfirmChange}
value={this.state.passPhraseConfirm} value={this.state.passPhraseConfirm}
className="mx_CreateKeyBackupDialog_passPhraseInput" className="mx_CreateKeyBackupDialog_passPhraseInput"
placeholder={_t("Repeat your recovery passphrase...")} placeholder={_t("Repeat your Security Phrase...")}
autoFocus={true} autoFocus={true}
/> />
</div> </div>
@ -338,15 +338,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseShowKey() { _renderPhaseShowKey() {
return <div> return <div>
<p>{_t( <p>{_t(
"Your recovery key is a safety net - you can use it to restore " + "Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your recovery passphrase.", "access to your encrypted messages if you forget your Security Phrase.",
)}</p> )}</p>
<p>{_t( <p>{_t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.", "Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p> )}</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer"> <div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader"> <div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your recovery key")} {_t("Your Security Key")}
</div> </div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer"> <div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey"> <div className="mx_CreateKeyBackupDialog_recoveryKey">
@ -369,12 +369,12 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let introText; let introText;
if (this.state.copied) { if (this.state.copied) {
introText = _t( introText = _t(
"Your recovery key has been <b>copied to your clipboard</b>, paste it to:", "Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
{}, {b: s => <b>{s}</b>}, {}, {b: s => <b>{s}</b>},
); );
} else if (this.state.downloaded) { } else if (this.state.downloaded) {
introText = _t( introText = _t(
"Your recovery key is in your <b>Downloads</b> folder.", "Your Security Key is in your <b>Downloads</b> folder.",
{}, {b: s => <b>{s}</b>}, {}, {b: s => <b>{s}</b>},
); );
} }
@ -433,14 +433,14 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_titleForPhase(phase) { _titleForPhase(phase) {
switch (phase) { switch (phase) {
case PHASE_PASSPHRASE: case PHASE_PASSPHRASE:
return _t('Secure your backup with a recovery passphrase'); return _t('Secure your backup with a Security Phrase');
case PHASE_PASSPHRASE_CONFIRM: case PHASE_PASSPHRASE_CONFIRM:
return _t('Confirm your recovery passphrase'); return _t('Confirm your Security Phrase');
case PHASE_OPTOUT_CONFIRM: case PHASE_OPTOUT_CONFIRM:
return _t('Warning!'); return _t('Warning!');
case PHASE_SHOWKEY: case PHASE_SHOWKEY:
case PHASE_KEEPITSAFE: case PHASE_KEEPITSAFE:
return _t('Make a copy of your recovery key'); return _t('Make a copy of your Security Key');
case PHASE_BACKINGUP: case PHASE_BACKINGUP:
return _t('Starting backup...'); return _t('Starting backup...');
case PHASE_DONE: case PHASE_DONE:

View file

@ -235,7 +235,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const blob = new Blob([this._recoveryKey.encodedPrivateKey], { const blob = new Blob([this._recoveryKey.encodedPrivateKey], {
type: 'text/plain;charset=us-ascii', type: 'text/plain;charset=us-ascii',
}); });
FileSaver.saveAs(blob, 'recovery-key.txt'); FileSaver.saveAs(blob, 'security-key.txt');
this.setState({ this.setState({
downloaded: true, downloaded: true,
@ -593,10 +593,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
onValidate={this._onPassPhraseValidate} onValidate={this._onPassPhraseValidate}
fieldRef={this._passphraseField} fieldRef={this._passphraseField}
autoFocus={true} autoFocus={true}
label={_td("Enter a recovery passphrase")} label={_td("Enter a Security Phrase")}
labelEnterPassword={_td("Enter a recovery passphrase")} labelEnterPassword={_td("Enter a Security Phrase")}
labelStrongPassword={_td("Great! This recovery passphrase looks strong enough.")} labelStrongPassword={_td("Great! This Security Phrase looks strong enough.")}
labelAllowedButUnsafe={_td("Great! This recovery passphrase looks strong enough.")} labelAllowedButUnsafe={_td("Great! This Security Phrase looks strong enough.")}
/> />
</div> </div>

View file

@ -58,7 +58,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
</span>; </span>;
const newMethodDetected = <p>{_t( const newMethodDetected = <p>{_t(
"A new recovery passphrase and key for Secure Messages have been detected.", "A new Security Phrase and key for Secure Messages have been detected.",
)}</p>; )}</p>;
const hackWarning = <p className="warning">{_t( const hackWarning = <p className="warning">{_t(

View file

@ -56,7 +56,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
> >
<div> <div>
<p>{_t( <p>{_t(
"This session has detected that your recovery passphrase and key " + "This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.", "for Secure Messages have been removed.",
)}</p> )}</p>
<p>{_t( <p>{_t(

View file

@ -397,8 +397,9 @@ export const toRightOf = (elementRect: DOMRect, chevronOffset = 12) => {
return {left, top, chevronOffset}; return {left, top, chevronOffset};
}; };
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect // Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect,
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None) => { // and either above or below: wherever there is more space (maybe this should be aboveOrBelowLeftOf?)
export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace }; const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset; const buttonRight = elementRect.right + window.pageXOffset;
@ -408,14 +409,49 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
menuOptions.right = window.innerWidth - buttonRight; menuOptions.right = window.innerWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available. // Align the menu vertically on whichever side of the button has more space available.
if (buttonBottom < window.innerHeight / 2) { if (buttonBottom < window.innerHeight / 2) {
menuOptions.top = buttonBottom; menuOptions.top = buttonBottom + vPadding;
} else { } else {
menuOptions.bottom = window.innerHeight - buttonTop; menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
} }
return menuOptions; return menuOptions;
}; };
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the left of elementRect
// and always above elementRect
export const alwaysAboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonRight = elementRect.right + window.pageXOffset;
const buttonBottom = elementRect.bottom + window.pageYOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the right edge of the menu to the right edge of the button
menuOptions.right = window.innerWidth - buttonRight;
// Align the menu vertically on whichever side of the button has more space available.
if (buttonBottom < window.innerHeight / 2) {
menuOptions.top = buttonBottom + vPadding;
} else {
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
}
return menuOptions;
};
// Placement method for <ContextMenu /> to position context menu right-aligned and flowing to the right of elementRect
// and always above elementRect
export const alwaysAboveRightOf = (elementRect: DOMRect, chevronFace = ChevronFace.None, vPadding = 0) => {
const menuOptions: IPosition & { chevronFace: ChevronFace } = { chevronFace };
const buttonLeft = elementRect.left + window.pageXOffset;
const buttonTop = elementRect.top + window.pageYOffset;
// Align the left edge of the menu to the left edge of the button
menuOptions.left = buttonLeft;
// Align the menu vertically above the menu
menuOptions.bottom = (window.innerHeight - buttonTop) + vPadding;
return menuOptions;
};
type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void]; type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => { export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
const button = useRef<T>(null); const button = useRef<T>(null);

View file

@ -45,7 +45,7 @@ class FilePanel extends React.Component {
}; };
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => { onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
if (room.roomId !== this.props.roomId) return; if (room?.roomId !== this.props?.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return; if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
if (ev.isBeingDecrypted()) { if (ev.isBeingDecrypted()) {

View file

@ -0,0 +1,56 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../views/context_menus/IconizedContextMenu";
import { _t } from "../../languageHandler";
import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig";
interface IProps {}
interface IState {}
export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => {
await HostSignupStore.instance.setHostSignupActive(true);
}
public render(): React.ReactNode {
const hostSignupConfig = SdkConfig.get().hostSignup;
if (!hostSignupConfig?.brand) {
return null;
}
return (
<IconizedContextMenuOptionList>
<IconizedContextMenuOption
iconClassName="mx_UserMenu_iconHosting"
label={_t(
"Upgrade to %(hostSignupBrand)s",
{
hostSignupBrand: hostSignupConfig.brand,
},
)}
onClick={this.openDialog}
/>
</IconizedContextMenuOptionList>
);
}
}

View file

@ -177,7 +177,14 @@ export default class InteractiveAuthComponent extends React.Component {
stageState: stageState, stageState: stageState,
errorText: stageState.error, errorText: stageState.error,
}, () => { }, () => {
if (oldStage != stageType) this._setFocus(); if (oldStage !== stageType) {
this._setFocus();
} else if (
!stageState.error && this._stageComponent.current &&
this._stageComponent.current.attemptFailed
) {
this._stageComponent.current.attemptFailed();
}
}); });
}; };

View file

@ -56,7 +56,7 @@ const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT); const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true); const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
useEffect(onResize, [expanded]); useEffect(onResize, [expanded, onResize]);
const [onFocus, isActive, ref] = useRovingTabIndex(); const [onFocus, isActive, ref] = useRovingTabIndex();
const tabIndex = isActive ? 0 : -1; const tabIndex = isActive ? 0 : -1;

View file

@ -54,6 +54,7 @@ import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPa
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal"; import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer';
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -140,7 +141,7 @@ class LoggedInView extends React.Component<IProps, IState> {
protected readonly _matrixClient: MatrixClient; protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>; protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>; protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
protected readonly _compactLayoutWatcherRef: string; protected compactLayoutWatcherRef: string;
protected resizer: Resizer; protected resizer: Resizer;
constructor(props, context) { constructor(props, context) {
@ -157,18 +158,6 @@ class LoggedInView extends React.Component<IProps, IState> {
CallMediaHandler.loadDevices(); CallMediaHandler.loadDevices();
document.addEventListener('keydown', this._onNativeKeyDown, false);
this._updateServerNoticeEvents();
this._matrixClient.on("accountData", this.onAccountData);
this._matrixClient.on("sync", this.onSync);
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
this._compactLayoutWatcherRef = SettingsStore.watchSetting(
"useCompactLayout", null, this.onCompactLayoutChanged,
);
fixupColorFonts(); fixupColorFonts();
this._roomView = React.createRef(); this._roomView = React.createRef();
@ -176,6 +165,24 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
componentDidMount() { componentDidMount() {
document.addEventListener('keydown', this._onNativeKeyDown, false);
this._updateServerNoticeEvents();
this._matrixClient.on("accountData", this.onAccountData);
this._matrixClient.on("sync", this.onSync);
// Call `onSync` with the current state as well
this.onSync(
this._matrixClient.getSyncState(),
null,
this._matrixClient.getSyncStateData(),
);
this._matrixClient.on("RoomState.events", this.onRoomStateEvents);
this.compactLayoutWatcherRef = SettingsStore.watchSetting(
"useCompactLayout", null, this.onCompactLayoutChanged,
);
this.resizer = this._createResizer(); this.resizer = this._createResizer();
this.resizer.attach(); this.resizer.attach();
this._loadResizerPreferences(); this._loadResizerPreferences();
@ -186,7 +193,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this._matrixClient.removeListener("accountData", this.onAccountData); this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync); this._matrixClient.removeListener("sync", this.onSync);
this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents); this._matrixClient.removeListener("RoomState.events", this.onRoomStateEvents);
SettingsStore.unwatchSetting(this._compactLayoutWatcherRef); SettingsStore.unwatchSetting(this.compactLayoutWatcherRef);
this.resizer.detach(); this.resizer.detach();
} }
@ -209,10 +216,12 @@ class LoggedInView extends React.Component<IProps, IState> {
_createResizer() { _createResizer() {
let size; let size;
let collapsed;
const collapseConfig: ICollapseConfig = { const collapseConfig: ICollapseConfig = {
toggleSize: 260 - 50, toggleSize: 260 - 50,
onCollapsed: (collapsed) => { onCollapsed: (_collapsed) => {
if (collapsed) { collapsed = _collapsed;
if (_collapsed) {
dis.dispatch({action: "hide_left_panel"}, true); dis.dispatch({action: "hide_left_panel"}, true);
window.localStorage.setItem("mx_lhs_size", '0'); window.localStorage.setItem("mx_lhs_size", '0');
} else { } else {
@ -227,7 +236,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this.props.resizeNotifier.startResizing(); this.props.resizeNotifier.startResizing();
}, },
onResizeStop: () => { onResizeStop: () => {
window.localStorage.setItem("mx_lhs_size", '' + size); if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
this.props.resizeNotifier.stopResizing(); this.props.resizeNotifier.stopResizing();
}, },
}; };
@ -419,6 +428,14 @@ class LoggedInView extends React.Component<IProps, IState> {
handled = true; handled = true;
} }
break; break;
case Key.F:
if (ctrlCmdOnly && SettingsStore.getValue("ctrlFForSearch")) {
dis.dispatch({
action: 'focus_search',
});
handled = true;
}
break;
case Key.BACKTICK: case Key.BACKTICK:
// Ideally this would be CTRL+P for "Profile", but that's // Ideally this would be CTRL+P for "Profile", but that's
// taken by the print dialog. CTRL+I for "Information" // taken by the print dialog. CTRL+I for "Information"
@ -632,6 +649,7 @@ class LoggedInView extends React.Component<IProps, IState> {
</div> </div>
<CallContainer /> <CallContainer />
<NonUrgentToastContainer /> <NonUrgentToastContainer />
<HostSignupContainer />
</MatrixClientContext.Provider> </MatrixClientContext.Provider>
); );
} }

View file

@ -34,7 +34,6 @@ import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg"; import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import * as RoomListSorter from "../../RoomListSorter";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier'; import Notifier from '../../Notifier';
@ -48,7 +47,6 @@ import * as Lifecycle from '../../Lifecycle';
// LifecycleStore is not used but does listen to and dispatch actions // LifecycleStore is not used but does listen to and dispatch actions
import '../../stores/LifecycleStore'; import '../../stores/LifecycleStore';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import { getHomePageUrl } from '../../utils/pages';
import createRoom from "../../createRoom"; import createRoom from "../../createRoom";
import {_t, _td, getCurrentLanguage} from '../../languageHandler'; import {_t, _td, getCurrentLanguage} from '../../languageHandler';
@ -82,6 +80,8 @@ import CreateCommunityPrototypeDialog from "../views/dialogs/CreateCommunityProt
import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore"; import ThreepidInviteStore, { IThreepidInvite, IThreepidInviteWireFormat } from "../../stores/ThreepidInviteStore";
import {UIFeature} from "../../settings/UIFeature"; import {UIFeature} from "../../settings/UIFeature";
import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore"; import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -219,6 +219,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private screenAfterLogin?: IScreen; private screenAfterLogin?: IScreen;
private windowWidth: number; private windowWidth: number;
private pageChanging: boolean; private pageChanging: boolean;
private tokenLogin?: boolean;
private accountPassword?: string; private accountPassword?: string;
private accountPasswordTimer?: NodeJS.Timeout; private accountPasswordTimer?: NodeJS.Timeout;
private focusComposer: boolean; private focusComposer: boolean;
@ -324,13 +325,21 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
Lifecycle.attemptTokenLogin( Lifecycle.attemptTokenLogin(
this.props.realQueryParams, this.props.realQueryParams,
this.props.defaultDeviceDisplayName, this.props.defaultDeviceDisplayName,
).then((loggedIn) => { this.getFragmentAfterLogin(),
if (loggedIn) { ).then(async (loggedIn) => {
if (this.props.realQueryParams?.loginToken) {
// remove the loginToken from the URL regardless
this.props.onTokenLoginCompleted(); this.props.onTokenLoginCompleted();
}
// don't do anything else until the page reloads - just stay in if (loggedIn) {
// the 'loading' state. this.tokenLogin = true;
return;
// Create and start the client
await Lifecycle.restoreFromLocalStorage({
ignoreGuest: true,
});
return this.postLoginSetup();
} }
// if the user has followed a login or register link, don't reanimate // if the user has followed a login or register link, don't reanimate
@ -354,6 +363,42 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
CountlyAnalytics.instance.enable(/* anonymous = */ true); CountlyAnalytics.instance.enable(/* anonymous = */ true);
} }
private async postLoginSetup() {
const cli = MatrixClientPeg.get();
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
}
const promisesList = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
promisesList.push(cli.downloadKeys([cli.getUserId()]));
}
// Now update the state to say we're waiting for the first sync to complete rather
// than for the login to finish.
this.setState({ pendingInitialSync: true });
await Promise.all(promisesList);
if (!cryptoEnabled) {
this.setState({ pendingInitialSync: false });
return;
}
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: false });
}
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
UNSAFE_componentWillUpdate(props, state) { UNSAFE_componentWillUpdate(props, state) {
@ -591,7 +636,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
MatrixClientPeg.get().leave(payload.room_id).then(() => { MatrixClientPeg.get().leave(payload.room_id).then(() => {
modal.close(); modal.close();
if (this.state.currentRoomId === payload.room_id) { if (this.state.currentRoomId === payload.room_id) {
dis.dispatch({action: 'view_next_room'}); dis.dispatch({action: 'view_home_page'});
} }
}, (err) => { }, (err) => {
modal.close(); modal.close();
@ -620,9 +665,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
break; break;
} }
case 'view_next_room':
this.viewNextRoom(1);
break;
case Action.ViewUserSettings: { case Action.ViewUserSettings: {
const tabPayload = payload as OpenToTabPayload; const tabPayload = payload as OpenToTabPayload;
const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog"); const UserSettingsDialog = sdk.getComponent("dialogs.UserSettingsDialog");
@ -708,8 +750,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.state.resizeNotifier.notifyLeftHandleResized(); this.state.resizeNotifier.notifyLeftHandleResized();
}); });
break; break;
case Action.OpenDialPad:
Modal.createTrackedDialog('Dial pad', '', DialPadModal, {}, "mx_Dialog_dialPadWrapper");
break;
case 'on_logged_in': case 'on_logged_in':
if ( if (
// Skip this handling for token login as that always calls onLoggedIn itself
!this.tokenLogin &&
!Lifecycle.isSoftLogout() && !Lifecycle.isSoftLogout() &&
this.state.view !== Views.LOGIN && this.state.view !== Views.LOGIN &&
this.state.view !== Views.REGISTER && this.state.view !== Views.REGISTER &&
@ -802,35 +849,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.notifyNewScreen('register'); this.notifyNewScreen('register');
} }
// TODO: Move to RoomViewStore
private viewNextRoom(roomIndexDelta: number) {
const allRooms = RoomListSorter.mostRecentActivityFirst(
MatrixClientPeg.get().getRooms(),
);
// If there are 0 rooms or 1 room, view the home page because otherwise
// if there are 0, we end up trying to index into an empty array, and
// if there is 1, we end up viewing the same room.
if (allRooms.length < 2) {
dis.dispatch({
action: 'view_home_page',
});
return;
}
let roomIndex = -1;
for (let i = 0; i < allRooms.length; ++i) {
if (allRooms[i].roomId === this.state.currentRoomId) {
roomIndex = i;
break;
}
}
roomIndex = (roomIndex + roomIndexDelta) % allRooms.length;
if (roomIndex < 0) roomIndex = allRooms.length - 1;
dis.dispatch({
action: 'view_room',
room_id: allRooms[roomIndex].roomId,
});
}
// switch view to the given room // switch view to the given room
// //
// @param {Object} roomInfo Object containing data about the room to be joined // @param {Object} roomInfo Object containing data about the room to be joined
@ -1097,9 +1115,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
private forgetRoom(roomId: string) { private forgetRoom(roomId: string) {
MatrixClientPeg.get().forget(roomId).then(() => { MatrixClientPeg.get().forget(roomId).then(() => {
// Switch to another room view if we're currently viewing the historical room // Switch to home page if we're currently viewing the forgotten room
if (this.state.currentRoomId === roomId) { if (this.state.currentRoomId === roomId) {
dis.dispatch({ action: "view_next_room" }); dis.dispatch({ action: "view_home_page" });
} }
}).catch((err) => { }).catch((err) => {
const errCode = err.errcode || _td("unknown error code"); const errCode = err.errcode || _td("unknown error code");
@ -1216,6 +1234,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
) { ) {
showAnalyticsToast(this.props.config.piwik?.policyUrl); showAnalyticsToast(this.props.config.piwik?.policyUrl);
} }
if (SdkConfig.get().mobileGuideToast) {
// The toast contains further logic to detect mobile platforms,
// check if it has been dismissed before, etc.
showMobileGuideToast();
}
} }
private showScreenAfterLogin() { private showScreenAfterLogin() {
@ -1233,12 +1256,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else { } else {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'view_welcome_page'}); dis.dispatch({action: 'view_welcome_page'});
} else if (getHomePageUrl(this.props.config)) {
dis.dispatch({action: 'view_home_page'});
} else { } else {
this.firstSyncPromise.promise.then(() => { dis.dispatch({action: 'view_home_page'});
dis.dispatch({action: 'view_next_room'});
});
} }
} }
} }
@ -1356,6 +1375,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on('Session.logged_out', function(errObj) { cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return; if (Lifecycle.isLoggingOut()) return;
// A modal might have been open when we were logged out by the server
Modal.closeCurrentModal('Session.logged_out');
if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) { if (errObj.httpStatus === 401 && errObj.data && errObj.data['soft_logout']) {
console.warn("Soft logout issued by server - avoiding data deletion"); console.warn("Soft logout issued by server - avoiding data deletion");
Lifecycle.softLogout(); Lifecycle.softLogout();
@ -1366,6 +1388,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
title: _t('Signed Out'), title: _t('Signed Out'),
description: _t('For security, this session has been signed out. Please sign in again.'), description: _t('For security, this session has been signed out. Please sign in again.'),
}); });
dis.dispatch({ dis.dispatch({
action: 'logout', action: 'logout',
}); });
@ -1635,10 +1658,16 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149 // TODO: Handle encoded room/event IDs: https://github.com/vector-im/element-web/issues/9149
let threepidInvite: IThreepidInvite; let threepidInvite: IThreepidInvite;
// if we landed here from a 3PID invite, persist it
if (params.signurl && params.email) { if (params.signurl && params.email) {
threepidInvite = ThreepidInviteStore.instance threepidInvite = ThreepidInviteStore.instance
.storeInvite(roomString, params as IThreepidInviteWireFormat); .storeInvite(roomString, params as IThreepidInviteWireFormat);
} }
// otherwise check that this room doesn't already have a known invite
if (!threepidInvite) {
const invites = ThreepidInviteStore.instance.getInvites();
threepidInvite = invites.find(invite => invite.roomId === roomString);
}
// on our URLs there might be a ?via=matrix.org or similar to help // on our URLs there might be a ?via=matrix.org or similar to help
// joins to the room succeed. We'll pass these through as an array // joins to the room succeed. We'll pass these through as an array
@ -1867,40 +1896,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// Create and start the client // Create and start the client
await Lifecycle.setLoggedIn(credentials); await Lifecycle.setLoggedIn(credentials);
await this.postLoginSetup();
const cli = MatrixClientPeg.get();
const cryptoEnabled = cli.isCryptoEnabled();
if (!cryptoEnabled) {
this.onLoggedIn();
}
const promisesList = [this.firstSyncPromise.promise];
if (cryptoEnabled) {
// wait for the client to finish downloading cross-signing keys for us so we
// know whether or not we have keys set up on this account
promisesList.push(cli.downloadKeys([cli.getUserId()]));
}
// Now update the state to say we're waiting for the first sync to complete rather
// than for the login to finish.
this.setState({ pendingInitialSync: true });
await Promise.all(promisesList);
if (!cryptoEnabled) {
this.setState({ pendingInitialSync: false });
return;
}
const crossSigningIsSetUp = cli.getStoredCrossSigningForUser(cli.getUserId());
if (crossSigningIsSetUp) {
this.setStateForNewView({ view: Views.COMPLETE_SECURITY });
} else if (await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) {
this.setStateForNewView({ view: Views.E2E_SETUP });
} else {
this.onLoggedIn();
}
this.setState({ pendingInitialSync: false });
}; };
// complete security / e2e setup has finished // complete security / e2e setup has finished
@ -1944,6 +1940,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
<E2eSetup <E2eSetup
onFinished={this.onCompleteSecurityE2eSetupFinished} onFinished={this.onCompleteSecurityE2eSetupFinished}
accountPassword={this.accountPassword} accountPassword={this.accountPassword}
tokenLogin={!!this.tokenLogin}
/> />
); );
} else if (this.state.view === Views.LOGGED_IN) { } else if (this.state.view === Views.LOGGED_IN) {

View file

@ -23,9 +23,11 @@ import classNames from 'classnames';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import {wantsDateSeparator} from '../../DateUtils'; import {wantsDateSeparator} from '../../DateUtils';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from "../../dispatcher/dispatcher";
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import SettingsStore from '../../settings/SettingsStore'; import SettingsStore from '../../settings/SettingsStore';
import {Layout, LayoutPropType} from "../../settings/Layout";
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent"; import {textForEvent} from "../../TextForEvent";
@ -135,14 +137,13 @@ export default class MessagePanel extends React.Component {
// whether to show reactions for an event // whether to show reactions for an event
showReactions: PropTypes.bool, showReactions: PropTypes.bool,
// whether to use the irc layout // which layout to use
useIRCLayout: PropTypes.bool, layout: LayoutPropType,
// whether or not to show flair at all // whether or not to show flair at all
enableFlair: PropTypes.bool, enableFlair: PropTypes.bool,
}; };
// Force props to be loaded for useIRCLayout
constructor(props) { constructor(props) {
super(props); super(props);
@ -207,11 +208,13 @@ export default class MessagePanel extends React.Component {
componentDidMount() { componentDidMount() {
this._isMounted = true; this._isMounted = true;
this.dispatcherRef = dis.register(this.onAction);
} }
componentWillUnmount() { componentWillUnmount() {
this._isMounted = false; this._isMounted = false;
SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef); SettingsStore.unwatchSetting(this._showTypingNotificationsWatcherRef);
dis.unregister(this.dispatcherRef);
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
@ -224,6 +227,14 @@ export default class MessagePanel extends React.Component {
} }
} }
onAction = (payload) => {
switch (payload.action) {
case "scroll_to_bottom":
this.scrollToBottom();
break;
}
}
onShowTypingNotificationsChange = () => { onShowTypingNotificationsChange = () => {
this.setState({ this.setState({
showTypingNotifications: SettingsStore.getValue("showTypingNotifications"), showTypingNotifications: SettingsStore.getValue("showTypingNotifications"),
@ -612,7 +623,7 @@ export default class MessagePanel extends React.Component {
isSelectedEvent={highlight} isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent} getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}
useIRCLayout={this.props.useIRCLayout} layout={this.props.layout}
enableFlair={this.props.enableFlair} enableFlair={this.props.enableFlair}
/> />
</TileErrorBoundary> </TileErrorBoundary>
@ -810,7 +821,7 @@ export default class MessagePanel extends React.Component {
} }
let ircResizer = null; let ircResizer = null;
if (this.props.useIRCLayout) { if (this.props.layout == Layout.IRC) {
ircResizer = <IRCTimelineProfileResizer ircResizer = <IRCTimelineProfileResizer
minWidth={20} minWidth={20}
maxWidth={600} maxWidth={600}

View file

@ -39,7 +39,7 @@ class NotificationPanel extends React.Component {
const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty"> const emptyState = (<div className="mx_RightPanel_empty mx_NotificationPanel_empty">
<h2>{_t('Youre all caught up')}</h2> <h2>{_t('Youre all caught up')}</h2>
<p>{_t('You have no visible notifications in this room.')}</p> <p>{_t('You have no visible notifications.')}</p>
</div>); </div>);
let content; let content;

Some files were not shown because too many files have changed in this diff Show more