Merge branch 'develop' into feature_confetti#14676

This commit is contained in:
Steffen Kolmer 2020-11-26 18:21:28 +01:00 committed by GitHub
commit 27312c3553
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
236 changed files with 13547 additions and 3882 deletions

View file

@ -1,36 +1,15 @@
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update. # autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
src/ImageUtils.js
src/Markdown.js src/Markdown.js
src/Rooms.js
src/Unread.js
src/Velociraptor.js src/Velociraptor.js
src/components/structures/RoomDirectory.js src/components/structures/RoomDirectory.js
src/components/structures/ScrollPanel.js
src/components/structures/UploadBar.js
src/components/views/elements/AddressSelector.js
src/components/views/elements/DirectorySearchBox.js
src/components/views/messages/MFileBody.js
src/components/views/messages/TextualBody.js
src/components/views/rooms/AuxPanel.js
src/components/views/rooms/LinkPreviewWidget.js
src/components/views/rooms/MemberList.js src/components/views/rooms/MemberList.js
src/components/views/rooms/RoomPreviewBar.js
src/components/views/settings/ChangeAvatar.js
src/components/views/settings/DevicesPanel.js
src/components/views/settings/Notifications.js
src/rageshake/rageshake.js
src/ratelimitedfunc.js src/ratelimitedfunc.js
src/utils/DMRoomMap.js src/utils/DMRoomMap.js
src/utils/DecryptFile.js
src/utils/DirectoryUtils.js
src/utils/MultiInviter.js src/utils/MultiInviter.js
src/utils/Receipt.js
test/components/structures/MessagePanel-test.js test/components/structures/MessagePanel-test.js
test/components/views/dialogs/InteractiveAuthDialog-test.js test/components/views/dialogs/InteractiveAuthDialog-test.js
test/mock-clock.js test/mock-clock.js
test/notifications/ContentRules-test.js
test/notifications/PushRuleVectorState-test.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/riot/

View file

@ -1,3 +1,259 @@
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)
* Upgrade JS SDK to 9.2.0
* [Release] Fix encrypted video playback in Chrome-based browsers
[\#5431](https://github.com/matrix-org/matrix-react-sdk/pull/5431)
Changes in [3.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0-rc.1) (2020-11-18)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0...v3.9.0-rc.1)
* Upgrade JS SDK to 9.2.0-rc.1
* Translations update from Weblate
[\#5429](https://github.com/matrix-org/matrix-react-sdk/pull/5429)
* Fix message search summary text
[\#5428](https://github.com/matrix-org/matrix-react-sdk/pull/5428)
* Shrink new room intro top margin to half for encryption bubble tile
[\#5427](https://github.com/matrix-org/matrix-react-sdk/pull/5427)
* Small delight tweaks to improve rough corners in the app
[\#5418](https://github.com/matrix-org/matrix-react-sdk/pull/5418)
* Fix DM logic to always pick a more reliable DM room
[\#5424](https://github.com/matrix-org/matrix-react-sdk/pull/5424)
* Update styling of the Analytics toast
[\#5408](https://github.com/matrix-org/matrix-react-sdk/pull/5408)
* Fix vertical centering of the Homepage and button layout
[\#5420](https://github.com/matrix-org/matrix-react-sdk/pull/5420)
* Fix BaseAvatar sometimes messing up and duplicating the url
[\#5422](https://github.com/matrix-org/matrix-react-sdk/pull/5422)
* Disable buttons when required by MSC2790
[\#5412](https://github.com/matrix-org/matrix-react-sdk/pull/5412)
* Fix drag drop file to upload for Safari
[\#5414](https://github.com/matrix-org/matrix-react-sdk/pull/5414)
* Fix poorly i18n'd string
[\#5416](https://github.com/matrix-org/matrix-react-sdk/pull/5416)
* Fix the feedback not closing without feedback/countly
[\#5417](https://github.com/matrix-org/matrix-react-sdk/pull/5417)
* Fix New Room Intro invite to this room button
[\#5419](https://github.com/matrix-org/matrix-react-sdk/pull/5419)
* Change how we expose Role in User Info and hide in DMs
[\#5413](https://github.com/matrix-org/matrix-react-sdk/pull/5413)
* Disallow sending of empty messages
[\#5390](https://github.com/matrix-org/matrix-react-sdk/pull/5390)
* hide some validation tooltips if fields are valid.
[\#5403](https://github.com/matrix-org/matrix-react-sdk/pull/5403)
* Improvements around new room empty space interactions
[\#5398](https://github.com/matrix-org/matrix-react-sdk/pull/5398)
* Implement call hold
[\#5366](https://github.com/matrix-org/matrix-react-sdk/pull/5366)
* Fix Skeleton UI showing up when not intended.
[\#5407](https://github.com/matrix-org/matrix-react-sdk/pull/5407)
* Close context menu when user clicks the Home button
[\#5406](https://github.com/matrix-org/matrix-react-sdk/pull/5406)
* Skip e2ee warn logout prompt if user has no megolm sessions to lose
[\#5410](https://github.com/matrix-org/matrix-react-sdk/pull/5410)
* Allow country names to be translated
[\#5405](https://github.com/matrix-org/matrix-react-sdk/pull/5405)
* Support thirdparty lookup for phone numbers
[\#5396](https://github.com/matrix-org/matrix-react-sdk/pull/5396)
* Change "Password" to "New Password"
[\#5371](https://github.com/matrix-org/matrix-react-sdk/pull/5371)
* Add customisation point for dehydration key
[\#5397](https://github.com/matrix-org/matrix-react-sdk/pull/5397)
* Rebrand Riot -> Element in the permalink classes
[\#5386](https://github.com/matrix-org/matrix-react-sdk/pull/5386)
* Invite / Create DM UX tweaks
[\#5387](https://github.com/matrix-org/matrix-react-sdk/pull/5387)
* Tweaks to toasts and post-registration landing
[\#5383](https://github.com/matrix-org/matrix-react-sdk/pull/5383)
Changes in [3.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0) (2020-11-09)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0-rc.1...v3.8.0)
* Upgrade JS SDK to 9.1.0
Changes in [3.8.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0-rc.1) (2020-11-04)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.1...v3.8.0-rc.1)
* Upgrade JS SDK to 9.1.0-rc.1
* Log when saving profile
[\#5394](https://github.com/matrix-org/matrix-react-sdk/pull/5394)
* Translations update from Weblate
[\#5395](https://github.com/matrix-org/matrix-react-sdk/pull/5395)
* Hide prompt to add email for notifications if 3pid ui feature is off
[\#5392](https://github.com/matrix-org/matrix-react-sdk/pull/5392)
* Fix room list message preview copy for hangup events
[\#5388](https://github.com/matrix-org/matrix-react-sdk/pull/5388)
* Track UISIs as Countly Events
[\#5382](https://github.com/matrix-org/matrix-react-sdk/pull/5382)
* Don't let users accidentally redact ACL events
[\#5384](https://github.com/matrix-org/matrix-react-sdk/pull/5384)
* Two more easy files to remove from eslintignore
[\#5378](https://github.com/matrix-org/matrix-react-sdk/pull/5378)
* Fix Widget OpenID Permissions for realsies
[\#5381](https://github.com/matrix-org/matrix-react-sdk/pull/5381)
* Fix regression with OpenID permissions on widgets
[\#5380](https://github.com/matrix-org/matrix-react-sdk/pull/5380)
* Fix room directory events happening in the wrong order for Funnels
[\#5379](https://github.com/matrix-org/matrix-react-sdk/pull/5379)
* Remove a couple more files from eslintignore
[\#5377](https://github.com/matrix-org/matrix-react-sdk/pull/5377)
* Fix countly method bindings and errors
[\#5376](https://github.com/matrix-org/matrix-react-sdk/pull/5376)
* Fix a bunch of silly lint errors
[\#5375](https://github.com/matrix-org/matrix-react-sdk/pull/5375)
* Typescript: ImageUtils
[\#5374](https://github.com/matrix-org/matrix-react-sdk/pull/5374)
* Convert AuxPanel to TypeScript
[\#5373](https://github.com/matrix-org/matrix-react-sdk/pull/5373)
* Only pass metrics if they exist otherwise Countly will be unhappy!
[\#5372](https://github.com/matrix-org/matrix-react-sdk/pull/5372)
* Fix CountlyAnalytics NPE on MatrixClientPeg
[\#5370](https://github.com/matrix-org/matrix-react-sdk/pull/5370)
* fix CountlyAnalytics canEnable on wrong target
[\#5369](https://github.com/matrix-org/matrix-react-sdk/pull/5369)
* Initial Countly work
[\#5365](https://github.com/matrix-org/matrix-react-sdk/pull/5365)
* Fix videos not playing in non-encrypted rooms
[\#5368](https://github.com/matrix-org/matrix-react-sdk/pull/5368)
* Fix custom tag layout which regressed in #5309
[\#5367](https://github.com/matrix-org/matrix-react-sdk/pull/5367)
* Watch replyToEvent at RoomView to prevent races
[\#5360](https://github.com/matrix-org/matrix-react-sdk/pull/5360)
* Add a UI Feature flag for room history settings
[\#5362](https://github.com/matrix-org/matrix-react-sdk/pull/5362)
* Hide inline images when preference disabled
[\#5361](https://github.com/matrix-org/matrix-react-sdk/pull/5361)
* Fix React warning by moving handler to each button
[\#5359](https://github.com/matrix-org/matrix-react-sdk/pull/5359)
* Do not preload encrypted videos|images unless autoplay or thumbnailing is on
[\#5352](https://github.com/matrix-org/matrix-react-sdk/pull/5352)
* Fix theme variable passed to Jitsi
[\#5357](https://github.com/matrix-org/matrix-react-sdk/pull/5357)
* docs: added comment explanation
[\#5349](https://github.com/matrix-org/matrix-react-sdk/pull/5349)
* Modal Widgets - MSC2790
[\#5252](https://github.com/matrix-org/matrix-react-sdk/pull/5252)
* Widgets fixes
[\#5350](https://github.com/matrix-org/matrix-react-sdk/pull/5350)
* Fix User Menu avatar colouring being based on wrong string
[\#5348](https://github.com/matrix-org/matrix-react-sdk/pull/5348)
* Support 'answered elsewhere'
[\#5345](https://github.com/matrix-org/matrix-react-sdk/pull/5345)
Changes in [3.7.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.1) (2020-10-28)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0...v3.7.1)
* Upgrade JS SDK to 9.0.1
* [Release] Fix theme variable passed to Jitsi
[\#5358](https://github.com/matrix-org/matrix-react-sdk/pull/5358)
* [Release] Widget fixes
[\#5351](https://github.com/matrix-org/matrix-react-sdk/pull/5351)
Changes in [3.7.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.0) (2020-10-26)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0-rc.2...v3.7.0)
* Upgrade JS SDK to 9.0.0
Changes in [3.7.0-rc.2](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.0-rc.2) (2020-10-21)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.7.0-rc.1...v3.7.0-rc.2)
* Fix JS SDK dependency to use 9.0.0-rc.1 as intended
Changes in [3.7.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.7.0-rc.1) (2020-10-21)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.1...v3.7.0-rc.1)
* Upgrade JS SDK to 9.0.0-rc.1
* Update Weblate URL
[\#5346](https://github.com/matrix-org/matrix-react-sdk/pull/5346)
* Translations update from Weblate
[\#5347](https://github.com/matrix-org/matrix-react-sdk/pull/5347)
* Left Panel Widget support
[\#5247](https://github.com/matrix-org/matrix-react-sdk/pull/5247)
* Pinned widgets work
[\#5266](https://github.com/matrix-org/matrix-react-sdk/pull/5266)
* Convert resizer to Typescript
[\#5343](https://github.com/matrix-org/matrix-react-sdk/pull/5343)
* Hide filtering microcopy when left panel is minimized
[\#5338](https://github.com/matrix-org/matrix-react-sdk/pull/5338)
* Skip editor confirmation of upgrades
[\#5344](https://github.com/matrix-org/matrix-react-sdk/pull/5344)
* Spec compliance, /search doesn't have to return results
[\#5337](https://github.com/matrix-org/matrix-react-sdk/pull/5337)
* Fix excessive hosting link padding
[\#5336](https://github.com/matrix-org/matrix-react-sdk/pull/5336)
* Adjust for new widget messaging APIs
[\#5341](https://github.com/matrix-org/matrix-react-sdk/pull/5341)
* Fix case where sublist context menu missed an update
[\#5339](https://github.com/matrix-org/matrix-react-sdk/pull/5339)
* Add analytics to VoIP
[\#5340](https://github.com/matrix-org/matrix-react-sdk/pull/5340)
* Fix Jitsi OpenIDC auth
[\#5334](https://github.com/matrix-org/matrix-react-sdk/pull/5334)
* Support rejecting calls
[\#5324](https://github.com/matrix-org/matrix-react-sdk/pull/5324)
* Don't show admin tooling if we're not in the room
[\#5330](https://github.com/matrix-org/matrix-react-sdk/pull/5330)
* Show Integrations error if iframe failed to load too
[\#5328](https://github.com/matrix-org/matrix-react-sdk/pull/5328)
* Add security customisation points
[\#5327](https://github.com/matrix-org/matrix-react-sdk/pull/5327)
* Discard all mx_fadable legacy cruft which is totally useless
[\#5326](https://github.com/matrix-org/matrix-react-sdk/pull/5326)
* Fix background-image: url(null) for backdrop filter
[\#5319](https://github.com/matrix-org/matrix-react-sdk/pull/5319)
* Make the ACL update message less noisy
[\#5316](https://github.com/matrix-org/matrix-react-sdk/pull/5316)
* Fix aspect ratio of avatar before clicking Save
[\#5318](https://github.com/matrix-org/matrix-react-sdk/pull/5318)
* Don't supply popout widgets with widget parameters
[\#5323](https://github.com/matrix-org/matrix-react-sdk/pull/5323)
* Changed rainbow algorithm
[\#5301](https://github.com/matrix-org/matrix-react-sdk/pull/5301)
* Renamed TagPanel and TagOrderStore
[\#5309](https://github.com/matrix-org/matrix-react-sdk/pull/5309)
* Fix/clarify boolean logic for reaction previews
[\#5321](https://github.com/matrix-org/matrix-react-sdk/pull/5321)
* Support glare for VoIP calls
[\#5311](https://github.com/matrix-org/matrix-react-sdk/pull/5311)
* Round of Typescript conversions
[\#5314](https://github.com/matrix-org/matrix-react-sdk/pull/5314)
* Fix broken rendering of Room Create when showHiddenEvents enabled
[\#5317](https://github.com/matrix-org/matrix-react-sdk/pull/5317)
* Improve LHS resize performance and tidy stale props&classes
[\#5313](https://github.com/matrix-org/matrix-react-sdk/pull/5313)
* event-index: Pass the user/device id pair when initializing the event index.
[\#5312](https://github.com/matrix-org/matrix-react-sdk/pull/5312)
* Fix various aspects of (jitsi) widgets
[\#5315](https://github.com/matrix-org/matrix-react-sdk/pull/5315)
* Fix rogue (partial) call bar
[\#5310](https://github.com/matrix-org/matrix-react-sdk/pull/5310)
* Rewrite call state machine
[\#5308](https://github.com/matrix-org/matrix-react-sdk/pull/5308)
* Convert `src/SecurityManager.js` to TypeScript
[\#5307](https://github.com/matrix-org/matrix-react-sdk/pull/5307)
* Fix templating for v1 jitsi widgets
[\#5305](https://github.com/matrix-org/matrix-react-sdk/pull/5305)
* Use new preparing event for widget communications
[\#5303](https://github.com/matrix-org/matrix-react-sdk/pull/5303)
* Fix parsing issue in event tile preview for appearance tab
[\#5302](https://github.com/matrix-org/matrix-react-sdk/pull/5302)
* Track replyToEvent along with Cider state & history
[\#5284](https://github.com/matrix-org/matrix-react-sdk/pull/5284)
* Roving Tab Index should not interfere with inputs
[\#5299](https://github.com/matrix-org/matrix-react-sdk/pull/5299)
* Visual tweaks from 2020-10-06 polishing
[\#5298](https://github.com/matrix-org/matrix-react-sdk/pull/5298)
* Convert auth lifecycle to TS, remove dead ILAG code
[\#5296](https://github.com/matrix-org/matrix-react-sdk/pull/5296)
Changes in [3.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.1) (2020-10-20) Changes in [3.6.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.1) (2020-10-20)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0...v3.6.1) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0...v3.6.1)

View file

@ -18,7 +18,7 @@ are currently filed against vector-im/element-web rather than this project).
Translation Status Translation Status
================== ==================
[![Translation status](https://translate.riot.im/widgets/element-web/-/multi-auto.svg)](https://translate.riot.im/engage/element-web/?utm_source=widget) [![Translation status](https://translate.element.io/widgets/element-web/-/multi-auto.svg)](https://translate.element.io/engage/element-web/?utm_source=widget)
Developer Guide Developer Guide
=============== ===============

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.6.1", "version": "3.9.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -79,7 +79,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"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.5", "matrix-widget-api": "^0.1.0-beta.10",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"pako": "^1.0.11", "pako": "^1.0.11",
"parse5": "^5.1.1", "parse5": "^5.1.1",

View file

@ -17,6 +17,7 @@ limitations under the License.
*/ */
@import "./_font-sizes.scss"; @import "./_font-sizes.scss";
@import "./_font-weights.scss";
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic $hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
@ -59,6 +60,10 @@ pre, code {
color: $accent-color; color: $accent-color;
} }
.text-muted {
color: $muted-fg-color;
}
b { b {
// On Firefox, the default weight for `<b>` is `bolder` which results in no bold // On Firefox, the default weight for `<b>` is `bolder` which results in no bold
// effect since we only have specific weights of our fonts available. // effect since we only have specific weights of our fonts available.
@ -323,6 +328,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_title { .mx_Dialog_title {
font-size: $font-22px; font-size: $font-22px;
font-weight: $font-semi-bold;
line-height: $font-36px; line-height: $font-36px;
color: $dialog-title-fg-color; color: $dialog-title-fg-color;
} }
@ -348,8 +354,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
background-color: $dialog-close-fg-color; background-color: $dialog-close-fg-color;
cursor: pointer; cursor: pointer;
position: absolute; position: absolute;
top: 4px; top: 10px;
right: 0px; right: 0;
} }
.mx_Dialog_content { .mx_Dialog_content {
@ -362,6 +368,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_buttons { .mx_Dialog_buttons {
margin-top: 20px; margin-top: 20px;
text-align: right; text-align: right;
.mx_Dialog_buttons_additive {
// The consumer is responsible for positioning their elements.
float: left;
}
} }
/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied /* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied

View file

@ -9,10 +9,12 @@
@import "./structures/_CustomRoomTagPanel.scss"; @import "./structures/_CustomRoomTagPanel.scss";
@import "./structures/_FilePanel.scss"; @import "./structures/_FilePanel.scss";
@import "./structures/_GenericErrorPage.scss"; @import "./structures/_GenericErrorPage.scss";
@import "./structures/_GroupFilterPanel.scss";
@import "./structures/_GroupView.scss"; @import "./structures/_GroupView.scss";
@import "./structures/_HeaderButtons.scss"; @import "./structures/_HeaderButtons.scss";
@import "./structures/_HomePage.scss"; @import "./structures/_HomePage.scss";
@import "./structures/_LeftPanel.scss"; @import "./structures/_LeftPanel.scss";
@import "./structures/_LeftPanelWidget.scss";
@import "./structures/_MainSplit.scss"; @import "./structures/_MainSplit.scss";
@import "./structures/_MatrixChat.scss"; @import "./structures/_MatrixChat.scss";
@import "./structures/_MyGroups.scss"; @import "./structures/_MyGroups.scss";
@ -26,7 +28,6 @@
@import "./structures/_ScrollPanel.scss"; @import "./structures/_ScrollPanel.scss";
@import "./structures/_SearchBox.scss"; @import "./structures/_SearchBox.scss";
@import "./structures/_TabbedView.scss"; @import "./structures/_TabbedView.scss";
@import "./structures/_GroupFilterPanel.scss";
@import "./structures/_ToastContainer.scss"; @import "./structures/_ToastContainer.scss";
@import "./structures/_UploadBar.scss"; @import "./structures/_UploadBar.scss";
@import "./structures/_UserMenu.scss"; @import "./structures/_UserMenu.scss";
@ -69,11 +70,13 @@
@import "./views/dialogs/_DeactivateAccountDialog.scss"; @import "./views/dialogs/_DeactivateAccountDialog.scss";
@import "./views/dialogs/_DevtoolsDialog.scss"; @import "./views/dialogs/_DevtoolsDialog.scss";
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss"; @import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
@import "./views/dialogs/_FeedbackDialog.scss";
@import "./views/dialogs/_GroupAddressPicker.scss"; @import "./views/dialogs/_GroupAddressPicker.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";
@import "./views/dialogs/_MessageEditHistoryDialog.scss"; @import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss"; @import "./views/dialogs/_NewSessionReviewDialog.scss";
@import "./views/dialogs/_RoomSettingsDialog.scss"; @import "./views/dialogs/_RoomSettingsDialog.scss";
@import "./views/dialogs/_RoomSettingsDialogBridges.scss"; @import "./views/dialogs/_RoomSettingsDialogBridges.scss";
@ -88,6 +91,7 @@
@import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
@import "./views/dialogs/security/_AccessSecretStorageDialog.scss"; @import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
@import "./views/dialogs/security/_CreateCrossSigningDialog.scss"; @import "./views/dialogs/security/_CreateCrossSigningDialog.scss";
@ -107,11 +111,11 @@
@import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_EventListSummary.scss";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_FormButton.scss"; @import "./views/elements/_FormButton.scss";
@import "./views/elements/_IconButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InfoTooltip.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";
@import "./views/elements/_ManageIntegsButton.scss"; @import "./views/elements/_ManageIntegsButton.scss";
@import "./views/elements/_MiniAvatarUploader.scss";
@import "./views/elements/_PowerSelector.scss"; @import "./views/elements/_PowerSelector.scss";
@import "./views/elements/_ProgressBar.scss"; @import "./views/elements/_ProgressBar.scss";
@import "./views/elements/_QRCode.scss"; @import "./views/elements/_QRCode.scss";
@ -136,6 +140,7 @@
@import "./views/groups/_GroupUserSettings.scss"; @import "./views/groups/_GroupUserSettings.scss";
@import "./views/messages/_CreateEvent.scss"; @import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss"; @import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_EventTileBubble.scss";
@import "./views/messages/_MEmoteBody.scss"; @import "./views/messages/_MEmoteBody.scss";
@import "./views/messages/_MFileBody.scss"; @import "./views/messages/_MFileBody.scss";
@import "./views/messages/_MImageBody.scss"; @import "./views/messages/_MImageBody.scss";
@ -179,6 +184,7 @@
@import "./views/rooms/_MemberList.scss"; @import "./views/rooms/_MemberList.scss";
@import "./views/rooms/_MessageComposer.scss"; @import "./views/rooms/_MessageComposer.scss";
@import "./views/rooms/_MessageComposerFormatBar.scss"; @import "./views/rooms/_MessageComposerFormatBar.scss";
@import "./views/rooms/_NewRoomIntro.scss";
@import "./views/rooms/_NotificationBadge.scss"; @import "./views/rooms/_NotificationBadge.scss";
@import "./views/rooms/_PinnedEventTile.scss"; @import "./views/rooms/_PinnedEventTile.scss";
@import "./views/rooms/_PinnedEventsPanel.scss"; @import "./views/rooms/_PinnedEventsPanel.scss";
@ -222,8 +228,9 @@
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss"; @import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss"; @import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
@import "./views/terms/_InlineTermsAgreement.scss"; @import "./views/terms/_InlineTermsAgreement.scss";
@import "./views/toasts/_AnalyticsToast.scss";
@import "./views/toasts/_NonUrgentEchoFailureToast.scss"; @import "./views/toasts/_NonUrgentEchoFailureToast.scss";
@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/_VideoView.scss"; @import "./views/voip/_VideoFeed.scss";

View file

@ -16,11 +16,6 @@ limitations under the License.
// TODO: Update design for custom tags to match new designs // TODO: Update design for custom tags to match new designs
.mx_LeftPanel_tagPanelContainer {
display: flex;
flex-direction: column;
}
.mx_CustomRoomTagPanel { .mx_CustomRoomTagPanel {
background-color: $groupFilterPanel-bg-color; background-color: $groupFilterPanel-bg-color;
max-height: 40vh; max-height: 40vh;

View file

@ -26,9 +26,10 @@ limitations under the License.
.mx_HomePage_default { .mx_HomePage_default {
text-align: center; text-align: center;
display: flex;
.mx_HomePage_default_wrapper { .mx_HomePage_default_wrapper {
padding: 25vh 0 12px; margin: auto;
} }
img { img {
@ -50,56 +51,54 @@ limitations under the License.
color: $muted-fg-color; color: $muted-fg-color;
} }
.mx_MiniAvatarUploader {
margin: 0 auto;
}
.mx_HomePage_default_buttons { .mx_HomePage_default_buttons {
margin: 80px auto 0; margin: 60px auto 0;
width: fit-content; width: fit-content;
.mx_AccessibleButton { .mx_AccessibleButton {
padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin
width: 104px; // 120px - 2* 8px width: 160px;
margin: 0 39px; // 55px - 2* 8px height: 132px;
margin: 20px;
position: relative; position: relative;
display: inline-block; display: inline-block;
border-radius: 8px; border-radius: 8px;
vertical-align: top; vertical-align: top;
word-break: break-word; word-break: break-word;
box-sizing: border-box;
font-weight: 600; font-weight: 600;
font-size: $font-15px; font-size: $font-15px;
line-height: $font-20px; line-height: $font-20px;
color: $muted-fg-color; color: #fff; // on all themes
background-color: $accent-color;
&:hover {
color: $accent-color;
background: rgba($accent-color, 0.06);
&::before {
background-color: $accent-color;
}
}
&::before { &::before {
top: 20px; top: 20px;
left: 40px; // (120px-40px)/2 left: 60px; // (160px-40px)/2
width: 40px; width: 40px;
height: 40px; height: 40px;
content: ''; content: '';
position: absolute; position: absolute;
background-color: $muted-fg-color; background-color: #fff; // on all themes
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-size: contain; mask-size: contain;
} }
&.mx_HomePage_button_sendDm::before { &.mx_HomePage_button_sendDm::before {
mask-image: url('$(res)/img/feather-customised/message-circle.svg'); mask-image: url('$(res)/img/element-icons/feedback.svg');
} }
&.mx_HomePage_button_explore::before { &.mx_HomePage_button_explore::before {
mask-image: url('$(res)/img/feather-customised/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
&.mx_HomePage_button_createGroup::before { &.mx_HomePage_button_createGroup::before {
mask-image: url('$(res)/img/feather-customised/group.svg'); mask-image: url('$(res)/img/element-icons/community-members.svg');
} }
} }
} }

View file

@ -32,6 +32,7 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
// Create another flexbox so the GroupFilterPanel fills the container // Create another flexbox so the GroupFilterPanel fills the container
display: flex; display: flex;
flex-direction: column;
// GroupFilterPanel handles its own CSS // GroupFilterPanel handles its own CSS
} }

View file

@ -0,0 +1,145 @@
/*
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_LeftPanelWidget {
// largely based on RoomSublist
margin-left: 8px;
margin-bottom: 4px;
.mx_LeftPanelWidget_headerContainer {
display: flex;
align-items: center;
height: 24px;
color: $roomlist-header-color;
margin-top: 4px;
.mx_LeftPanelWidget_stickable {
flex: 1;
max-width: 100%;
display: flex;
align-items: center;
}
.mx_LeftPanelWidget_headerText {
flex: 1;
max-width: calc(100% - 16px);
line-height: $font-16px;
font-size: $font-13px;
font-weight: 600;
// Ellipsize any text overflow
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
.mx_LeftPanelWidget_collapseBtn {
display: inline-block;
position: relative;
width: 14px;
height: 14px;
margin-right: 6px;
&::before {
content: '';
width: 18px;
height: 18px;
position: absolute;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
background-color: $roomlist-header-color;
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
&.mx_LeftPanelWidget_collapseBtn_collapsed::before {
transform: rotate(-90deg);
}
}
}
}
.mx_LeftPanelWidget_resizeBox {
position: relative;
display: flex;
flex-direction: column;
overflow: visible; // let the resize handle out
}
.mx_AppTileFullWidth {
flex: 1 0 0;
overflow: hidden;
// need this to be flex otherwise the overflow hidden from above
// sometimes vertically centers the clipped list ... no idea why it would do this
// as the box model should be top aligned. Happens in both FF and Chromium
display: flex;
flex-direction: column;
box-sizing: border-box;
mask-image: linear-gradient(0deg, transparent, black 4px);
}
.mx_LeftPanelWidget_resizerHandle {
cursor: ns-resize;
border-radius: 3px;
// Override styles from library
width: unset !important;
height: 4px !important;
position: absolute;
top: -24px !important; // override from library - puts it in the margin-top of the headerContainer
// 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_LeftPanelWidget_resizerHandle {
opacity: 0.8;
background-color: $primary-fg-color;
}
.mx_LeftPanelWidget_maximizeButton {
margin-left: 8px;
margin-right: 7px;
position: relative;
width: 24px;
height: 24px;
border-radius: 32px;
&::before {
content: '';
width: 16px;
height: 16px;
position: absolute;
top: 4px;
left: 4px;
mask-position: center;
mask-size: contain;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
background: $muted-fg-color;
}
}
}
.mx_LeftPanelWidget_maximizeButtonTooltip {
margin-top: -3px;
}

View file

@ -19,57 +19,6 @@ limitations under the License.
min-height: 50px; min-height: 50px;
} }
/* position the indicator in the same place horizontally as .mx_EventTile_avatar. */
.mx_RoomStatusBar_indicator {
padding-left: 17px;
padding-right: 12px;
margin-left: -73px;
margin-top: 15px;
float: left;
width: 24px;
text-align: center;
}
.mx_RoomStatusBar_callBar {
height: 50px;
line-height: $font-50px;
}
.mx_RoomStatusBar_placeholderIndicator span {
color: $primary-fg-color;
opacity: 0.5;
position: relative;
top: -4px;
/*
animation-duration: 1s;
animation-name: bounce;
animation-direction: alternate;
animation-iteration-count: infinite;
*/
}
.mx_RoomStatusBar_placeholderIndicator span:nth-child(1) {
animation-delay: 0.3s;
}
.mx_RoomStatusBar_placeholderIndicator span:nth-child(2) {
animation-delay: 0.6s;
}
.mx_RoomStatusBar_placeholderIndicator span:nth-child(3) {
animation-delay: 0.9s;
}
@keyframes bounce {
from {
opacity: 0.5;
top: 0;
}
to {
opacity: 0.2;
top: -3px;
}
}
.mx_RoomStatusBar_typingIndicatorAvatars { .mx_RoomStatusBar_typingIndicatorAvatars {
width: 52px; width: 52px;
margin-top: -1px; margin-top: -1px;
@ -153,16 +102,6 @@ limitations under the License.
display: block; display: block;
} }
.mx_RoomStatusBar_isAlone {
height: 50px;
line-height: $font-50px;
color: $primary-fg-color;
opacity: 0.5;
overflow-y: hidden;
display: block;
}
.mx_MatrixChat_useCompactLayout { .mx_MatrixChat_useCompactLayout {
.mx_RoomStatusBar { .mx_RoomStatusBar {
min-height: 40px; min-height: 40px;
@ -172,11 +111,6 @@ limitations under the License.
margin-top: 10px; margin-top: 10px;
} }
.mx_RoomStatusBar_callBar {
height: 40px;
line-height: $font-40px;
}
.mx_RoomStatusBar_typingBar { .mx_RoomStatusBar_typingBar {
height: 40px; height: 40px;
line-height: $font-40px; line-height: $font-40px;

View file

@ -14,6 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_InteractiveAuthEntryComponents_emailWrapper {
padding-right: 60px;
position: relative;
margin-top: 32px;
margin-bottom: 32px;
&::before, &::after {
position: absolute;
width: 116px;
height: 116px;
content: "";
right: -10px;
}
&::before {
background-color: rgba(244, 246, 250, 0.91);
border-radius: 50%;
top: -20px;
}
&::after {
background-image: url('$(res)/img/element-icons/email-prompt.svg');
background-repeat: no-repeat;
background-position: center;
background-size: contain;
top: -25px;
}
}
.mx_InteractiveAuthEntryComponents_msisdnWrapper { .mx_InteractiveAuthEntryComponents_msisdnWrapper {
text-align: center; text-align: center;
} }

View file

@ -41,7 +41,7 @@ limitations under the License.
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
object-fit: cover; object-fit: cover;
border-radius: 40px; border-radius: 125px;
vertical-align: top; vertical-align: top;
background-color: $avatar-bg-color; background-color: $avatar-bg-color;
} }

View file

@ -0,0 +1,121 @@
/*
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_FeedbackDialog {
hr {
margin: 24px 0;
border-color: $input-border-color;
}
.mx_Dialog_content {
margin-bottom: 24px;
> h2 {
margin-bottom: 32px;
}
}
.mx_FeedbackDialog_section {
position: relative;
padding-left: 52px;
> p {
color: $tertiary-fg-color;
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
a, .mx_AccessibleButton_kind_link {
color: $accent-color;
text-decoration: underline;
}
&::before, &::after {
content: "";
position: absolute;
width: 40px;
height: 40px;
left: 0;
top: 0;
}
&::before {
background-color: $icon-button-color;
border-radius: 20px;
}
&::after {
background: $avatar-initial-color; // TODO
mask-position: center;
mask-size: 24px;
mask-repeat: no-repeat;
}
}
.mx_FeedbackDialog_reportBug {
&::after {
mask-image: url('$(res)/img/feather-customised/bug.svg');
}
}
.mx_FeedbackDialog_rateApp {
.mx_RadioButton {
display: inline-flex;
font-size: 20px;
transition: font-size 1s, border .5s;
border-radius: 50%;
border: 2px solid transparent;
margin-top: 12px;
margin-bottom: 24px;
vertical-align: top;
cursor: pointer;
input[type="radio"] + div {
display: none;
}
.mx_RadioButton_content {
background: $icon-button-color;
width: 40px;
height: 40px;
text-align: center;
line-height: 40px;
border-radius: 20px;
margin: 5px;
}
.mx_RadioButton_spacer {
display: none;
}
& + .mx_RadioButton {
margin-left: 16px;
}
}
.mx_RadioButton_checked {
font-size: 24px;
border-color: $accent-color;
}
&::after {
mask-image: url('$(res)/img/element-icons/feedback.svg');
}
}
}

View file

@ -27,37 +27,29 @@ limitations under the License.
padding-left: 8px; padding-left: 8px;
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: auto;
display: flex;
flex-wrap: wrap;
.mx_InviteDialog_userTile { .mx_InviteDialog_userTile {
margin: 6px 6px 0 0;
display: inline-block; display: inline-block;
float: left; min-width: max-content; // prevent manipulation by flexbox
position: relative;
top: 7px;
} }
// Using a textarea for this element, to circumvent autofill // Mostly copied from AddressPickerDialog; overrides bunch of our default text input styles
// Mostly copied from AddressPickerDialog > input[type="text"] {
textarea, margin: 6px 0 !important;
textarea:focus { height: 24px;
height: 34px; line-height: $font-24px;
line-height: $font-34px;
font-size: $font-14px; font-size: $font-14px;
padding-left: 12px; padding-left: 12px;
margin: 0 !important;
border: 0 !important; border: 0 !important;
outline: 0 !important; outline: 0 !important;
resize: none; resize: none;
overflow: hidden;
box-sizing: border-box; box-sizing: border-box;
word-wrap: nowrap; min-width: 40%;
flex: 1 !important;
// Roughly fill about 2/5ths of the available space. This is to try and 'fill' the color: $primary-fg-color !important;
// remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have
// support for "fill remaining width", but traditional tricks don't work with what
// we're pushing into this "field". Flexbox just makes things worse. The theory is
// that users won't need more than about 2/5ths of the input to find the person
// they're looking for.
width: 40%;
} }
} }
@ -148,6 +140,10 @@ limitations under the License.
} }
} }
.mx_InviteDialog_roomTile_nameStack {
display: inline-block;
}
.mx_InviteDialog_roomTile_name { .mx_InviteDialog_roomTile_name {
font-weight: 600; font-weight: 600;
font-size: $font-14px; font-size: $font-14px;

View file

@ -0,0 +1,42 @@
/*
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_ModalWidgetDialog {
.mx_ModalWidgetDialog_warning {
margin-bottom: 24px;
> img {
vertical-align: middle;
margin-right: 8px;
}
}
.mx_ModalWidgetDialog_buttons {
float: right;
margin-top: 24px;
.mx_AccessibleButton + .mx_AccessibleButton {
margin-left: 8px;
}
}
iframe {
width: 100%;
height: 450px;
border: 0;
border-radius: 8px;
}
}

View file

@ -0,0 +1,75 @@
/*
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_WidgetCapabilitiesPromptDialog {
.text-muted {
font-size: $font-12px;
}
.mx_Dialog_content {
margin-bottom: 16px;
}
.mx_WidgetCapabilitiesPromptDialog_cap {
margin-top: 20px;
font-size: $font-15px;
line-height: $font-15px;
.mx_WidgetCapabilitiesPromptDialog_byline {
color: $muted-fg-color;
margin-left: 26px;
font-size: $font-12px;
line-height: $font-12px;
}
}
.mx_Dialog_buttons {
margin-top: 40px; // double normal
}
.mx_SettingsFlag {
line-height: calc($font-14px + 7px + 7px); // 7px top & bottom padding
color: $muted-fg-color;
font-size: $font-12px;
.mx_ToggleSwitch {
display: inline-block;
vertical-align: middle;
margin-right: 8px;
// downsize the switch + ball
width: $font-32px;
height: $font-15px;
&.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball {
left: calc(100% - $font-15px);
}
.mx_ToggleSwitch_ball {
width: $font-15px;
height: $font-15px;
border-radius: $font-15px;
}
}
.mx_SettingsFlag_label {
display: inline-block;
vertical-align: middle;
}
}
}

View file

@ -25,7 +25,7 @@ limitations under the License.
.mx_AccessibleButton_hasKind { .mx_AccessibleButton_hasKind {
padding: 7px 18px; padding: 7px 18px;
text-align: center; text-align: center;
border-radius: 4px; border-radius: 8px;
display: inline-block; display: inline-block;
font-size: $font-14px; font-size: $font-14px;
} }

View file

@ -1,55 +0,0 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_IconButton {
width: 32px;
height: 32px;
border-radius: 100%;
background-color: $accent-bg-color;
// don't shrink or grow if in a flex container
flex: 0 0 auto;
&.mx_AccessibleButton_disabled {
background-color: none;
&::before {
background-color: lightgrey;
}
}
&:hover {
opacity: 90%;
}
&::before {
content: "";
display: block;
width: 100%;
height: 100%;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 55%;
background-color: $accent-color;
}
&.mx_IconButton_icon_check::before {
mask-image: url('$(res)/img/feather-customised/check.svg');
}
&.mx_IconButton_icon_edit::before {
mask-image: url('$(res)/img/feather-customised/edit.svg');
}
}

View file

@ -0,0 +1,56 @@
/*
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_MiniAvatarUploader {
position: relative;
width: min-content;
&::before, &::after {
content: '';
position: absolute;
height: 26px;
width: 26px;
right: -6px;
bottom: -6px;
}
&::before {
background-color: $primary-bg-color;
border-radius: 50%;
z-index: 1;
}
&::after {
background-color: $secondary-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-image: url('$(res)/img/element-icons/camera.svg');
mask-size: 16px;
z-index: 2;
}
&.mx_MiniAvatarUploader_busy::after {
background: url("$(res)/img/spinner.gif") no-repeat center;
background-size: 80%;
mask: unset;
}
}
.mx_MiniAvatarUploader_input {
display: none;
}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,25 +15,8 @@ limitations under the License.
*/ */
.mx_CreateEvent { .mx_CreateEvent {
background-color: $info-plinth-bg-color; &::before {
padding-left: 20px; background-color: $composer-e2e-icon-color;
padding-right: 20px; mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
padding-top: 10px; }
padding-bottom: 10px;
}
.mx_CreateEvent_image {
float: left;
margin-right: 20px;
width: 72px;
height: 34px;
background-color: $primary-fg-color;
mask: url('$(res)/img/room-continuation.svg');
mask-repeat: no-repeat;
mask-position: center;
}
.mx_CreateEvent_header {
font-weight: bold;
} }

View file

@ -0,0 +1,60 @@
/*
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_EventTileBubble {
background-color: $dark-panel-bg-color;
padding: 10px;
border-radius: 8px;
margin: 10px auto;
max-width: 75%;
box-sizing: border-box;
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content;
&::before, &::after {
position: relative;
grid-column: 1;
grid-row: 1 / 3;
width: 16px;
height: 16px;
content: "";
top: 0;
bottom: 0;
left: 0;
right: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
margin-top: 4px;
}
.mx_EventTileBubble_title, .mx_EventTileBubble_subtitle {
overflow-wrap: break-word;
}
.mx_EventTileBubble_title {
font-weight: 600;
font-size: $font-15px;
grid-column: 2;
grid-row: 1;
}
.mx_EventTileBubble_subtitle {
font-size: $font-12px;
grid-column: 2;
grid-row: 2;
}
}

View file

@ -15,41 +15,8 @@ limitations under the License.
*/ */
.mx_MJitsiWidgetEvent { .mx_MJitsiWidgetEvent {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content;
&::before { &::before {
grid-column: 1;
grid-row: 1 / 3;
width: 16px;
height: 16px;
content: "";
top: 0;
bottom: 0;
left: 0;
right: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
background-color: $composer-e2e-icon-color; // XXX: Variable abuse background-color: $composer-e2e-icon-color; // XXX: Variable abuse
margin-top: 4px;
mask-image: url('$(res)/img/element-icons/call/video-call.svg'); mask-image: url('$(res)/img/element-icons/call/video-call.svg');
} }
.mx_MJitsiWidgetEvent_title {
font-weight: 600;
font-size: $font-15px;
grid-column: 2;
grid-row: 1;
}
.mx_MJitsiWidgetEvent_subtitle {
grid-column: 2;
grid-row: 2;
}
.mx_MJitsiWidgetEvent_title,
.mx_MJitsiWidgetEvent_subtitle {
overflow-wrap: break-word;
}
} }

View file

@ -18,5 +18,6 @@ span.mx_MVideoBody {
video.mx_MVideoBody { video.mx_MVideoBody {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
border-radius: 4px;
} }
} }

View file

@ -15,28 +15,6 @@ limitations under the License.
*/ */
.mx_cryptoEvent { .mx_cryptoEvent {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) min-content;
&.mx_cryptoEvent_icon::before,
&.mx_cryptoEvent_icon::after {
grid-column: 1;
grid-row: 1 / 3;
width: 16px;
height: 16px;
content: "";
top: 0;
bottom: 0;
left: 0;
right: 0;
mask-repeat: no-repeat;
mask-position: center;
mask-size: contain;
mask-image: url('$(res)/img/e2e/normal.svg');
background-color: $composer-e2e-icon-color;
margin-top: 4px;
}
// white infill for the transparency // white infill for the transparency
&.mx_cryptoEvent_icon::before { &.mx_cryptoEvent_icon::before {
background-color: #ffffff; background-color: #ffffff;
@ -46,6 +24,11 @@ limitations under the License.
mask-size: 90%; mask-size: 90%;
} }
&.mx_cryptoEvent_icon::after {
mask-image: url('$(res)/img/e2e/normal.svg');
background-color: $composer-e2e-icon-color;
}
&.mx_cryptoEvent_icon_verified::after { &.mx_cryptoEvent_icon_verified::after {
mask-image: url("$(res)/img/e2e/verified.svg"); mask-image: url("$(res)/img/e2e/verified.svg");
background-color: $accent-color; background-color: $accent-color;
@ -56,25 +39,6 @@ limitations under the License.
background-color: $notice-primary-color; background-color: $notice-primary-color;
} }
.mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state {
overflow-wrap: break-word;
}
.mx_cryptoEvent_title {
font-weight: 600;
font-size: $font-15px;
grid-column: 2;
grid-row: 1;
}
.mx_cryptoEvent_subtitle {
grid-column: 2;
grid-row: 2;
}
.mx_cryptoEvent_state, .mx_cryptoEvent_subtitle {
font-size: $font-12px;
}
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons { .mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
grid-column: 3; grid-column: 3;
@ -92,5 +56,7 @@ limitations under the License.
margin: auto 0; margin: auto 0;
text-align: center; text-align: center;
color: $notice-secondary-color; color: $notice-secondary-color;
overflow-wrap: break-word;
font-size: $font-12px;
} }
} }

View file

@ -173,26 +173,12 @@ limitations under the License.
margin: 6px 0; margin: 6px 0;
.mx_IconButton, .mx_Spinner {
margin-left: 20px;
width: 16px;
height: 16px;
&::before {
mask-size: 80%;
}
}
.mx_UserInfo_roleDescription { .mx_UserInfo_roleDescription {
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
// try to make it the same height as the dropdown // try to make it the same height as the dropdown
margin: 11px 0 12px 0; margin: 11px 0 12px 0;
.mx_IconButton {
margin-left: 6px;
}
} }
.mx_Field { .mx_Field {

View file

@ -25,15 +25,6 @@ $left-gutter: 64px;
position: relative; position: relative;
} }
.mx_EventTile_bubble {
background-color: $dark-panel-bg-color;
padding: 10px;
border-radius: 5px;
margin: 10px auto;
max-width: 75%;
box-sizing: border-box;
}
.mx_EventTile.mx_EventTile_info { .mx_EventTile.mx_EventTile_info {
padding-top: 0px; padding-top: 0px;
} }
@ -131,9 +122,10 @@ $left-gutter: 64px;
grid-template-columns: 1fr 100px; grid-template-columns: 1fr 100px;
.mx_EventTile_line { .mx_EventTile_line {
margin-right: 0px; margin-right: 0;
grid-column: 1 / 3; grid-column: 1 / 3;
padding: 0; // override default padding of mx_EventTile_line so that we can be centered
padding: 0 !important;
} }
.mx_EventTile_msgOption { .mx_EventTile_msgOption {

View file

@ -186,6 +186,7 @@ $irc-line-height: $font-18px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-width: var(--name-width); min-width: var(--name-width);
text-align: end;
} }
} }
} }

View file

@ -0,0 +1,67 @@
/*
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_NewRoomIntro {
margin: 40px 0 48px 64px;
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
&::before, &::after {
content: unset;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
.mx_NewRoomIntro_buttons {
margin-top: 28px;
.mx_AccessibleButton {
line-height: $font-24px;
&::before {
content: '';
display: inline-block;
background-color: $button-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 20px;
width: 20px;
height: 20px;
margin-right: 5px;
vertical-align: text-bottom;
}
}
.mx_NewRoomIntro_inviteButton::before {
mask-image: url('$(res)/img/element-icons/room/invite.svg');
}
}
> h2 {
margin-top: 24px;
font-size: $font-24px;
font-weight: 600;
}
> p {
margin: 0;
font-size: $font-15px;
color: $secondary-fg-color;
}
}

View file

@ -33,7 +33,6 @@ limitations under the License.
div:first-child { div:first-child {
font-weight: $font-semi-bold; font-weight: $font-semi-bold;
margin-bottom: 8px;
} }
.mx_AccessibleButton { .mx_AccessibleButton {
@ -41,6 +40,7 @@ limitations under the License.
position: relative; position: relative;
padding: 0 0 0 24px; padding: 0 0 0 24px;
font-size: inherit; font-size: inherit;
margin-top: 8px;
&::before { &::before {
content: ''; content: '';
@ -53,6 +53,13 @@ limitations under the License.
mask-position: center; mask-position: center;
mask-size: contain; mask-size: contain;
mask-repeat: no-repeat; mask-repeat: no-repeat;
}
&.mx_RoomList_explorePrompt_startChat::before {
mask-image: url('$(res)/img/element-icons/feedback.svg');
}
&.mx_RoomList_explorePrompt_explore::before {
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg'); mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
} }
} }

View file

@ -59,10 +59,6 @@ limitations under the License.
width: calc(100% - 22px); width: calc(100% - 22px);
} }
&.mx_RoomSublist_headerContainer_stickyBottom {
bottom: 0;
}
// We don't have a top style because the top is dependent on the room list header's // We don't have a top style because the top is dependent on the room list header's
// height, and is therefore calculated in JS. // height, and is therefore calculated in JS.
// The class, mx_RoomSublist_headerContainer_stickyTop, is applied though. // The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
@ -387,3 +383,22 @@ limitations under the License.
.mx_RoomSublist_addRoomTooltip { .mx_RoomSublist_addRoomTooltip {
margin-top: -3px; margin-top: -3px;
} }
.mx_RoomSublist_skeletonUI {
position: relative;
margin-left: 4px;
height: 288px;
&::before {
background: $roomsublist-skeleton-ui-bg;
width: 100%;
height: 100%;
content: '';
position: absolute;
mask-repeat: repeat-y;
mask-size: auto 48px;
mask-image: url('$(res)/img/element-icons/roomlist/skeleton-ui.svg');
}
}

View file

@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; .mx_AnalyticsToast {
.mx_AccessibleButton_kind_danger {
background: none;
color: $accent-color;
}
interface IProps { .mx_AccessibleButton_kind_primary {
background: $accent-color;
color: #ffffff;
}
} }
const PulsedAvatar: React.FC<IProps> = (props) => {
return <div className="mx_PulsedAvatar">
{props.children}
</div>;
};
export default PulsedAvatar;

View file

@ -33,11 +33,11 @@ limitations under the License.
pointer-events: initial; // restore pointer events so the user can leave/interact pointer-events: initial; // restore pointer events so the user can leave/interact
cursor: pointer; cursor: pointer;
.mx_VideoView { .mx_CallView_video {
width: 350px; width: 350px;
} }
.mx_VideoView_localVideoFeed { .mx_VideoFeed_local {
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }

View file

@ -15,80 +15,196 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_CallView_voice { .mx_CallView {
background-color: $accent-color; border-radius: 10px;
color: $accent-fg-color; background-color: $input-lighter-bg-color;
cursor: pointer; padding-left: 8px;
padding: 6px; padding-right: 8px;
font-weight: bold; // XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
pointer-events: initial;
}
border-radius: 8px; .mx_CallView_large {
min-width: 200px; padding-bottom: 10px;
display: flex; .mx_CallView_voice {
align-items: center; height: 360px;
img {
margin: 4px;
margin-right: 10px;
}
> div {
display: flex;
flex-direction: column;
// Hacky vertical align
padding-top: 3px;
}
> div > p,
> div > h1 {
padding: 0;
margin: 0;
font-size: $font-13px;
line-height: $font-15px;
}
> div > p {
font-weight: bold;
}
> * {
flex-grow: 0;
flex-shrink: 0;
} }
} }
.mx_CallView_hangup { .mx_CallView_pip {
position: absolute; width: 320px;
right: 8px; .mx_CallView_voice {
bottom: 10px; height: 180px;
}
}
height: 35px; .mx_CallView_voice {
width: 35px; position: relative;
display: flex;
align-items: center;
justify-content: center;
background-color: $inverted-bg-color;
}
border-radius: 35px; .mx_CallView_video {
width: 100%;
position: relative;
z-index: 30;
}
background-color: $notice-primary-color; .mx_CallView_header {
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
z-index: 101; .mx_BaseAvatar {
margin-right: 12px;
}
}
.mx_CallView_header_callType {
font-weight: bold;
vertical-align: middle;
}
.mx_CallView_header_controls {
margin-left: auto;
}
.mx_CallView_header_button {
display: inline-block;
vertical-align: middle;
cursor: pointer; cursor: pointer;
&::before { &::before {
content: ''; content: '';
position: absolute; display: inline-block;
height: 20px; height: 20px;
width: 20px; width: 20px;
vertical-align: middle;
top: 6.5px; background-color: $secondary-fg-color;
left: 7.5px; mask-repeat: no-repeat;
mask: url('$(res)/img/hangup.svg');
mask-size: contain; mask-size: contain;
background-size: contain; mask-position: center;
background-color: $primary-fg-color;
} }
} }
.mx_CallView_header_button_fullscreen {
&::before {
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
}
}
.mx_CallView_header_button_expand {
&::before {
mask-image: url('$(res)/img/element-icons/call/expand.svg');
}
}
.mx_CallView_header_roomName {
font-weight: bold;
font-size: 12px;
line-height: initial;
}
.mx_CallView_header_callTypeSmall {
font-size: 12px;
color: $secondary-fg-color;
line-height: initial;
}
.mx_CallView_header_phoneIcon {
display: inline-block;
margin-right: 6px;
height: 16px;
width: 16px;
vertical-align: middle;
&::before {
content: '';
display: inline-block;
vertical-align: top;
height: 16px;
width: 16px;
background-color: $warning-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}
.mx_CallView_callControls {
position: absolute;
display: flex;
justify-content: center;
bottom: 5px;
width: 100%;
opacity: 1;
transition: opacity 0.5s;
}
.mx_CallView_callControls_hidden {
opacity: 0.001; // opacity 0 can cause a re-layout
pointer-events: none;
}
.mx_CallView_callControls_button {
cursor: pointer;
margin-left: 8px;
margin-right: 8px;
&::before {
content: '';
display: inline-block;
height: 48px;
width: 48px;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
}
}
.mx_CallView_callControls_button_micOn {
&::before {
background-image: url('$(res)/img/voip/mic-on.svg');
}
}
.mx_CallView_callControls_button_micOff {
&::before {
background-image: url('$(res)/img/voip/mic-off.svg');
}
}
.mx_CallView_callControls_button_vidOn {
&::before {
background-image: url('$(res)/img/voip/vid-on.svg');
}
}
.mx_CallView_callControls_button_vidOff {
&::before {
background-image: url('$(res)/img/voip/vid-off.svg');
}
}
.mx_CallView_callControls_button_hangup {
&::before {
background-image: url('$(res)/img/voip/hangup.svg');
}
}
.mx_CallView_callControls_button_invisible {
visibility: hidden;
pointer-events: none;
position: absolute;
}

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,36 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_VideoView { .mx_VideoFeed_remote {
width: 100%;
position: relative;
z-index: 30;
}
.mx_VideoView video {
width: 100%;
}
.mx_VideoView_remoteVideoFeed {
width: 100%; width: 100%;
background-color: #000; background-color: #000;
z-index: 50; z-index: 50;
} }
.mx_VideoView_localVideoFeed { .mx_VideoFeed_local {
width: 25%; width: 25%;
height: 25%; height: 25%;
position: absolute; position: absolute;
left: 10px; right: 10px;
bottom: 10px; top: 10px;
z-index: 100; z-index: 100;
border-radius: 4px;
} }
.mx_VideoView_localVideoFeed video { .mx_VideoFeed_mirror {
width: auto;
height: 100%;
}
.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
transform: scale(-1, 1); transform: scale(-1, 1);
} }

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="M6 19H18C18.55 19 19 18.55 19 18V6C19 5.45 18.55 5 18 5H13C12.45 5 12 4.55 12 4C12 3.45 12.45 3 13 3H19C20.11 3 21 3.9 21 5V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V13C3 12.45 3.45 12 4 12C4.55 12 5 12.45 5 13V18C5 18.55 5.45 19 6 19ZM10 4C10 4.55 9.55 5 9 5H6.41L15.54 14.13C15.93 14.52 15.93 15.15 15.54 15.54C15.15 15.93 14.52 15.93 14.13 15.54L5 6.41V9C5 9.55 4.55 10 4 10C3.45 10 3 9.55 3 9V4C3 3.44772 3.44772 3 4 3H9C9.55 3 10 3.45 10 4Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

View file

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 2L19 20" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 4H4.41122L17 16.615V19.415C16.9914 19.4057 16.9825 19.3965 16.9735 19.3874L1.98909 4.37176C1.93823 4.32079 1.88324 4.27646 1.82519 4.23876C2.18599 4.08505 2.58306 4 3 4ZM0.386676 5.52565C0.140502 5.96107 0 6.46413 0 7V17C0 18.6569 1.34315 20 3 20H14.7593L0.573407 5.78449C0.495634 5.70656 0.433392 5.619 0.386676 5.52565ZM17 7V13.7837L7.2367 4H14C15.6569 4 17 5.34315 17 7Z" fill="white"/>
<path d="M19 9L22.3753 6.29976C23.0301 5.77595 24 6.24212 24 7.08062V16.9194C24 17.7579 23.0301 18.2241 22.3753 17.7002L19 15V9Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 791 B

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3L21 21" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6.59209V12C8 14.2091 9.79086 16 12 16C13.4616 16 14.7402 15.216 15.4381 14.0457L8 6.59209ZM16.8804 15.491C15.7918 17.0102 14.0115 18 12 18C8.68629 18 6 15.3137 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 16.0796 7.05369 19.446 11 19.9381V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V19.9381C15.1511 19.6699 17.037 18.5476 18.3077 16.9213L16.8804 15.491ZM19.3589 15.1433L17.7917 13.5729C17.9275 13.0716 18 12.5443 18 12C18 11.4477 18.4477 11 19 11C19.5523 11 20 11.4477 20 12C20 13.1159 19.7715 14.1783 19.3589 15.1433ZM16 11.7774L8.43077 4.19238C9.09091 2.89149 10.4413 2 12 2C14.2091 2 16 3.79086 16 6V11.7774Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 913 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6V12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12V6ZM5 11C5.55228 11 6 11.4477 6 12C6 15.3137 8.68629 18 12 18C15.3137 18 18 15.3137 18 12C18 11.4477 18.4477 11 19 11C19.5523 11 20 11.4477 20 12C20 16.0796 16.9463 19.446 13 19.9381V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V19.9381C7.05369 19.446 4 16.0796 4 12C4 11.4477 4.44772 11 5 11Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 587 B

View file

@ -0,0 +1,10 @@
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4896 2.5C9.04778 2.5 7.827 3.52171 7.54879 4.90624C7.50711 5.11367 7.42679 5.31408 7.28726 5.47312L6.6851 6.15949C6.49523 6.37591 6.22129 6.5 5.93338 6.5H2.75C1.64543 6.5 0.75 7.39543 0.75 8.5V19.5C0.75 20.6046 1.64543 21.5 2.75 21.5H22.75C23.8546 21.5 24.75 20.6046 24.75 19.5V8.5C24.75 7.39543 23.8546 6.5 22.75 6.5H19.5666C19.2787 6.5 19.0048 6.37591 18.8149 6.15949L18.2127 5.47312C18.0732 5.31408 17.9929 5.11366 17.9512 4.90623C17.673 3.5217 16.4522 2.5 15.0104 2.5H10.4896ZM16.75 13.5C16.75 15.7091 14.9591 17.5 12.75 17.5C10.5409 17.5 8.75 15.7091 8.75 13.5C8.75 11.2909 10.5409 9.5 12.75 9.5C14.9591 9.5 16.75 11.2909 16.75 13.5ZM3.25 5C2.97386 5 2.75 5.22386 2.75 5.5C2.75 5.77614 2.97386 6 3.25 6H5.25C5.52614 6 5.75 5.77614 5.75 5.5C5.75 5.22386 5.52614 5 5.25 5H3.25Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="24" height="24" fill="white" transform="translate(0.75)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,11 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.90964 11.5297C9.09231 11.5297 11.6724 8.94865 11.6724 5.76483C11.6724 2.581 9.09231 0 5.90964 0C2.72697 0 0.146904 2.581 0.146904 5.76483C0.146904 6.65678 0.3494 7.50142 0.710912 8.25525L0.0648767 10.3556C-0.171716 11.1248 0.550948 11.8442 1.31906 11.6041L3.39724 10.9544C4.15657 11.323 5.00898 11.5297 5.90964 11.5297Z" fill="black"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.03851 12.8449C5.70399 13.1151 6.4314 13.2638 7.19345 13.2638C10.3676 13.2638 13.5 10.6832 13.5 7.49979C13.5 6.63255 13.2676 5.81005 12.8651 5.07227C14.6487 6.05071 15.8583 7.94999 15.8583 10.1326C15.8583 11.0243 15.6564 11.8688 15.2959 12.6224L15.9404 14.7232C16.1765 15.4926 15.4533 16.2114 14.6854 15.9708L12.6155 15.322C11.8585 15.6902 11.0088 15.8966 10.111 15.8966C7.91459 15.8966 6.00594 14.661 5.03851 12.8449Z" fill="black"/>
</g>
<defs>
<clipPath id="clip0">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,13 @@
<svg width="57" height="77" viewBox="0 0 57 77" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55298 38.9352H4C1.79086 38.9352 0 40.726 0 42.9352V72.0304C0 74.2396 1.79086 76.0304 4 76.0304H53C55.2091 76.0304 57 74.2396 57 72.0304V42.9352C57 40.726 55.2091 38.9352 53 38.9352H51.365V41.6473H5.55298V38.9352ZM26.9753 61.3068L3.10141 43.4482C2.33137 42.8721 2.73876 41.6474 3.70041 41.6474H28.459H53.3841C54.3282 41.6474 54.7464 42.8352 54.0107 43.4268L31.8776 61.2212C30.4545 62.3653 28.4374 62.4005 26.9753 61.3068Z" fill="#8A8C8E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.5885 33.0898C48.9384 33.2156 48.2703 33.2911 47.5885 33.3119V34.706V44.4238V54.1415H49.5885V44.4238V34.706V33.0898ZM36.5604 14.2706H13.7177C10.9562 14.2706 8.71765 16.5092 8.71765 19.2706V34.706V44.4238V54.1415H10.7177V44.4238V34.706V19.2706C10.7177 17.6138 12.0608 16.2706 13.7177 16.2706H35.5616C35.8354 15.571 36.1706 14.9022 36.5604 14.2706Z" fill="#8A8C8E"/>
<path d="M16.6589 30.5414H37.4826" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16.2706" y1="37.8708" x2="40.6473" y2="37.8708" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16.2706" y1="44.812" x2="40.6473" y2="44.812" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="47.2003" cy="20.8237" r="9.71771" fill="#FE2928"/>
<rect x="45.812" y="14.5765" width="2.77649" height="8.32946" rx="1" fill="white"/>
<rect x="45.812" y="24.2943" width="2.77649" height="2.77649" rx="1" fill="white"/>
<line x1="27.3766" y1="1" x2="27.3766" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="34.3179" y1="6.55298" x2="34.3179" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="20.4354" y1="6.55298" x2="20.4354" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

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 fill-rule="evenodd" clip-rule="evenodd" d="M12.283 21.4392C17.649 21.4392 21.9991 17.0875 21.9991 11.7196C21.9991 6.3516 17.649 2 12.283 2C6.91698 2 2.56696 6.3516 2.56696 11.7196C2.56696 13.2233 2.90831 14.6472 3.51772 15.9181L2.04565 20.7041C1.80906 21.4733 2.53172 22.1926 3.29983 21.9525L8.04605 20.4688C9.32655 21.0905 10.7641 21.4392 12.283 21.4392Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 481 B

View file

@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 9C16 9 17 10.2857 17 12C17 13.7143 16 15 16 15" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M19 6C19 6 21 8.57143 21 12C21 15.4286 19 18 19 18" stroke="white" stroke-width="2" stroke-linecap="round"/>
<rect x="2" y="8" width="11" height="8" rx="2" fill="white"/>
<path d="M7 8L11.3598 4.36682C12.0111 3.82405 13 4.2872 13 5.13504V18.865C13 19.7128 12.0111 20.176 11.3598 19.6332L7 16V8Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 541 B

View file

@ -0,0 +1,5 @@
<svg width="228" height="48" viewBox="0 0 228 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="16" cy="16" r="16" fill="#D4D4D4"/>
<rect x="39" width="189" height="12" rx="6" fill="#D4D4D4"/>
<rect x="39" y="20" width="143" height="12" rx="6" fill="#D4D4D4"/>
</svg>

After

Width:  |  Height:  |  Size: 282 B

View file

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#737D8C" style="mix-blend-mode:multiply"/>
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

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 fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.23858 2 7 4.23858 7 7L17 7C17 4.23858 14.7614 2 12 2ZM2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L6.41421 9H17.5858L20.2929 6.29289C20.6834 5.90237 21.3166 5.90237 21.7071 6.29289C22.0976 6.68342 22.0976 7.31658 21.7071 7.70711L19 10.4142V13H22C22.5523 13 23 13.4477 23 14C23 14.5523 22.5523 15 22 15H19C19 15.7795 18.8726 16.5292 18.6375 17.2295C18.6614 17.2493 18.6847 17.2705 18.7071 17.2929L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L17.6791 19.0933C16.5924 20.5983 14.9222 21.6542 13 21.9291L13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V21.9291C9.07785 21.6542 7.40759 20.5983 6.32091 19.0933L3.70711 21.7071C3.31658 22.0976 2.68342 22.0976 2.29289 21.7071C1.90237 21.3166 1.90237 20.6834 2.29289 20.2929L5.29289 17.2929C5.31533 17.2705 5.33857 17.2493 5.36252 17.2295C5.1274 16.5292 5 15.7795 5 15H2C1.44772 15 1 14.5523 1 14C1 13.4477 1.44772 13 2 13H5V10.4142L2.29289 7.70711Z" fill="#FF4B55"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="25px" height="26px" viewBox="-1 -1 25 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.3 (16618) - http://www.bohemiancoding.com/sketch -->
<title>Fill 72 + Path 98</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="05-Voice-and-video" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="05_2-Video-call" sketch:type="MSArtboardGroup" transform="translate(-910.000000, -719.000000)" stroke="#FF0064">
<g id="Fill-72-+-Path-98" sketch:type="MSLayerGroup" transform="translate(910.000000, 719.000000)">
<path d="M17.8404444,24 C15.8137778,24 10.8875556,21.408 6.75022222,15.8448889 C2.88088889,10.6413333 1,6.88222222 1,4.35244444 C1,2.36088889 2.37511111,1.41022222 3.11422222,0.9 L3.29644444,0.772888889 C4.11288889,0.188888889 5.38222222,0 5.86888889,0 C6.72222222,0 7.08177778,0.499555556 7.29955556,0.935111111 C7.48488889,1.30311111 9.01777778,4.59511111 9.17288889,5.00444444 C9.41111111,5.63377778 9.33288889,6.55111111 8.596,7.07822222 L8.46622222,7.16844444 C8.10044444,7.42222222 7.42,7.89333333 7.32577778,8.46622222 C7.28,8.74488889 7.37333333,9.03644444 7.61111111,9.35688889 C8.79777778,10.956 12.5862222,15.6506667 13.2693333,16.2884444 C13.8044444,16.7884444 14.4826667,16.8595556 14.9444444,16.4702222 C15.4222222,16.0675556 15.6342222,15.8297778 15.6364444,15.8271111 L15.6857778,15.7795556 C15.7257778,15.7457778 16.0991111,15.4497778 16.7093333,15.4497778 C17.1497778,15.4497778 17.5973333,15.6017778 18.04,15.9008889 C19.1884444,16.6768889 21.7808889,18.4106667 21.7808889,18.4106667 L21.8226667,18.4426667 C22.1542222,18.7266667 22.6333333,19.5453333 22.0751111,20.6106667 C21.496,21.7168889 19.6986667,24 17.8404444,24 L17.8404444,24 Z" id="Fill-72" sketch:type="MSShapeGroup"></path>
<path d="M19.8035085,4 L0,22.8035085" id="Path-98" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -1,6 +0,0 @@
<svg width="72" height="34" viewBox="0 0 72 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 7.26087V1H28.7889V7.26087M1 7.26087V33H28.7889V7.26087M1 7.26087H28.7889M4.16583 4.13043H16.8291" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
<path d="M43.2109 7.26087V1H70.9999V7.26087M43.2109 7.26087V33H70.9999V7.26087M43.2109 7.26087H70.9999M46.3768 4.13043H59.0401" stroke="#454545" stroke-width="2" stroke-linejoin="round"/>
<path d="M27.03 28.8262C34.2226 28.8262 36.0207 26.343 36.0207 25.1014V16.0996C36.0207 12.1264 43.6283 11.3401 47.432 11.4436" stroke="black" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 623 B

17
res/img/voip/hangup.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="#FE2928"/>
</g>
<path d="m 24.008382,14.7565 c -1.6873,-0.0649 -5.157,0.373 -6.0006,0.5948 -0.0499,0.0131 -0.1074,0.0278 -0.1716,0.0442 -1.2952,0.3306 -5.35348,1.3663 -5.79196,4.6481 -0.33971,2.5426 1.36151,3.3122 2.21196,3.195 0.5886,-0.0738 2.2739,-0.3403 3.831,-0.6197 1.5291,-0.2743 1.5283,-1.283 1.5278,-1.9651 0,-0.0125 0,-0.025 0,-0.0373 v -1.3712 c 0,-0.3492 0.3281,-0.5511 0.7808,-0.6057 1.6024,-0.2176 2.9401,-0.2183 3.6097,-0.2183 h 0.0057 c 0.6695,0 1.9906,7e-4 3.593,0.2183 0.4527,0.0546 0.7808,0.2565 0.7808,0.6057 v 1.3712 c 0,0.0124 0,0.0248 0,0.0373 -5e-4,0.6821 -0.0013,1.6908 1.5278,1.9652 1.5571,0.2793 3.2424,0.5458 3.831,0.6196 0.8504,0.1172 2.5517,-0.6524 2.212,-3.195 -0.4385,-3.2818 -4.4968,-4.3175 -5.792,-4.6481 -0.0642,-0.0164 -0.1217,-0.031 -0.1716,-0.0442 -0.8435,-0.2218 -4.2966,-0.6597 -5.9838,-0.5948 z" fill="#ffffff" />
<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.6 KiB

18
res/img/voip/mic-off.svg Normal file
View file

@ -0,0 +1,18 @@
<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="#61708B"/>
</g>
<path d="M15 11L33 29" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 14.5921V20C20 22.2091 21.7909 24 24 24C25.4616 24 26.7402 23.216 27.4381 22.0457L20 14.5921ZM28.8804 23.491C27.7918 25.0102 26.0115 26 24 26C20.6863 26 18 23.3137 18 20C18 19.4477 17.5523 19 17 19C16.4477 19 16 19.4477 16 20C16 24.0796 19.0537 27.446 23 27.9381V29C23 29.5523 23.4477 30 24 30C24.5523 30 25 29.5523 25 29V27.9381C27.1511 27.6699 29.037 26.5476 30.3077 24.9213L28.8804 23.491ZM31.3589 23.1433L29.7917 21.5729C29.9275 21.0716 30 20.5443 30 20C30 19.4477 30.4477 19 31 19C31.5523 19 32 19.4477 32 20C32 21.1159 31.7715 22.1783 31.3589 23.1433ZM28 19.7774L20.4308 12.1924C21.0909 10.8915 22.4413 10 24 10C26.2091 10 28 11.7909 28 14V19.7774Z" fill="white"/>
<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.6 KiB

17
res/img/voip/mic-on.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="M20 14C20 11.7909 21.7909 10 24 10C26.2091 10 28 11.7909 28 14V20C28 22.2091 26.2091 24 24 24C21.7909 24 20 22.2091 20 20V14ZM17 19C17.5523 19 18 19.4477 18 20C18 23.3137 20.6863 26 24 26C27.3137 26 30 23.3137 30 20C30 19.4477 30.4477 19 31 19C31.5523 19 32 19.4477 32 20C32 24.0796 28.9463 27.446 25 27.9381V29C25 29.5523 24.5523 30 24 30C23.4477 30 23 29.5523 23 29V27.9381C19.0537 27.446 16 24.0796 16 20C16 19.4477 16.4477 19 17 19Z" fill="#61708B"/>
<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

19
res/img/voip/vid-off.svg Normal file
View file

@ -0,0 +1,19 @@
<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="#61708B"/>
</g>
<path d="M14.8334 11.6666L29.8334 26.6666" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 13.3333H17.676L28.1667 23.8458V26.1791C28.1595 26.1713 28.1521 26.1636 28.1446 26.1561L15.662 13.6474C16.0648 13.4464 16.5192 13.3333 17 13.3333ZM14.4359 14.7751C14.1593 15.2292 14 15.7626 14 16.3332V23.6666C14 25.3234 15.3431 26.6666 17 26.6666H26.2994L14.4778 14.8203C14.4632 14.8056 14.4492 14.7906 14.4359 14.7751ZM28.1667 16.3333V21.4863L20.0306 13.3333H25.1667C26.8235 13.3333 28.1667 14.6764 28.1667 16.3333Z" fill="white"/>
<path d="M29.8334 17.5L32.3753 15.4664C33.0301 14.9426 34 15.4087 34 16.2473V23.7527C34 24.5912 33.0301 25.0573 32.3753 24.5335L29.8334 22.5V17.5Z" fill="white"/>
<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.5 KiB

18
res/img/voip/vid-on.svg Normal file
View file

@ -0,0 +1,18 @@
<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="M14 16.3334C14 14.6765 15.3431 13.3334 17 13.3334H25.1667C26.8235 13.3334 28.1667 14.6765 28.1667 16.3334V23.6667C28.1667 25.3236 26.8235 26.6667 25.1667 26.6667H17C15.3431 26.6667 14 25.3236 14 23.6667V16.3334Z" fill="#61708B"/>
<path d="M29.8334 17.5001L32.3753 15.4665C33.0301 14.9427 34 15.4089 34 16.2474V23.7528C34 24.5913 33.0301 25.0575 32.3753 24.5337L29.8334 22.5001V17.5001Z" fill="#61708B"/>
<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.2 KiB

View file

@ -117,6 +117,7 @@ $roomlist-filter-active-bg-color: $bg-color;
$roomlist-bg-color: rgba(33, 38, 44, 0.90); $roomlist-bg-color: rgba(33, 38, 44, 0.90);
$roomlist-header-color: $tertiary-fg-color; $roomlist-header-color: $tertiary-fg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;
@ -273,6 +274,10 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
background-color: #080808; background-color: #080808;
} }
} }
blockquote {
color: #919191;
}
} }
// diff highlight colors // diff highlight colors

View file

@ -114,6 +114,7 @@ $roomlist-filter-active-bg-color: $roomlist-button-bg-color;
$roomlist-bg-color: $header-panel-bg-color; $roomlist-bg-color: $header-panel-bg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;

View file

@ -181,6 +181,7 @@ $roomlist-filter-active-bg-color: $roomlist-button-bg-color;
$roomlist-bg-color: $header-panel-bg-color; $roomlist-bg-color: $header-panel-bg-color;
$roomlist-header-color: $primary-fg-color; $roomlist-header-color: $primary-fg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;

View file

@ -175,6 +175,7 @@ $roomlist-filter-active-bg-color: #ffffff;
$roomlist-bg-color: rgba(245, 245, 245, 0.90); $roomlist-bg-color: rgba(245, 245, 245, 0.90);
$roomlist-header-color: $tertiary-fg-color; $roomlist-header-color: $tertiary-fg-color;
$roomsublist-divider-color: $primary-fg-color; $roomsublist-divider-color: $primary-fg-color;
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
$groupFilterPanel-divider-color: $roomlist-header-color; $groupFilterPanel-divider-color: $roomlist-header-color;

View file

@ -33,7 +33,9 @@ import RightPanelStore from "../stores/RightPanelStore";
import WidgetStore from "../stores/WidgetStore"; import WidgetStore from "../stores/WidgetStore";
import CallHandler from "../CallHandler"; import CallHandler from "../CallHandler";
import {Analytics} from "../Analytics"; import {Analytics} from "../Analytics";
import CountlyAnalytics from "../CountlyAnalytics";
import UserActivity from "../UserActivity"; import UserActivity from "../UserActivity";
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
declare global { declare global {
interface Window { interface Window {
@ -59,12 +61,21 @@ declare global {
mxWidgetStore: WidgetStore; mxWidgetStore: WidgetStore;
mxCallHandler: CallHandler; mxCallHandler: CallHandler;
mxAnalytics: Analytics; mxAnalytics: Analytics;
mxCountlyAnalytics: typeof CountlyAnalytics;
mxUserActivity: UserActivity; mxUserActivity: UserActivity;
mxModalWidgetStore: ModalWidgetStore;
} }
interface Document { interface Document {
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess // https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
hasStorageAccess?: () => Promise<boolean>; hasStorageAccess?: () => Promise<boolean>;
// Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now
webkitExitFullscreen(): Promise<void>;
msExitFullscreen(): Promise<void>;
readonly webkitFullscreenElement: Element | null;
readonly msFullscreenElement: Element | null;
} }
interface Navigator { interface Navigator {
@ -94,4 +105,20 @@ declare global {
interface HTMLAudioElement { interface HTMLAudioElement {
type?: string; type?: string;
} }
interface Element {
// Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
}
interface Error {
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
fileName?: string;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/lineNumber
lineNumber?: number;
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber
columnNumber?: number;
}
} }

View file

@ -24,6 +24,7 @@ import {ActionPayload} from "./dispatcher/payloads";
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload"; 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";
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";
@ -105,6 +106,9 @@ export default abstract class BasePlatform {
* @param newVersion the version string to check * @param newVersion the version string to check
*/ */
protected shouldShowUpdate(newVersion: string): boolean { protected shouldShowUpdate(newVersion: string): boolean {
// If the user registered on this client in the last 24 hours then do not show them the update toast
if (MatrixClientPeg.userRegisteredWithinLastHours(24)) return false;
try { try {
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY)); const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
return newVersion !== version || Date.now() > deferUntil; return newVersion !== version || Date.now() > deferUntil;

View file

@ -59,8 +59,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg'; import PlatformPeg from './PlatformPeg';
import Modal from './Modal'; import Modal from './Modal';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Matrix from 'matrix-js-sdk';
import dis from './dispatcher/dispatcher'; 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';
@ -77,8 +76,10 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import WidgetStore from "./stores/WidgetStore"; import WidgetStore from "./stores/WidgetStore";
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore"; import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions"; import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/lib/webrtc/call"; import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
import Analytics from './Analytics'; import Analytics from './Analytics';
import CountlyAnalytics from "./CountlyAnalytics";
import {UIFeature} from "./settings/UIFeature";
enum AudioID { enum AudioID {
Ring = 'ringAudio', Ring = 'ringAudio',
@ -97,6 +98,21 @@ export enum PlaceCallType {
ScreenSharing = 'screensharing', ScreenSharing = 'screensharing',
} }
function getRemoteAudioElement(): HTMLAudioElement {
// this needs to be somewhere at the top of the DOM which
// always exists to avoid audio interruptions.
// Might as well just use DOM.
const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
if (!remoteAudioElement) {
console.error(
"Failed to find remoteAudio element - cannot play audio!" +
"You need to add an <audio/> to the DOM.",
);
return null;
}
return remoteAudioElement;
}
export default class CallHandler { export default class CallHandler {
private calls = new Map<string, MatrixCall>(); private calls = new Map<string, MatrixCall>();
private audioPromises = new Map<AudioID, Promise<void>>(); private audioPromises = new Map<AudioID, Promise<void>>();
@ -109,7 +125,7 @@ export default class CallHandler {
return window.mxCallHandler; return window.mxCallHandler;
} }
constructor() { start() {
dis.register(this.onAction); 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
@ -122,6 +138,27 @@ export default class CallHandler {
navigator.mediaSession.setActionHandler('previoustrack', function() {}); navigator.mediaSession.setActionHandler('previoustrack', function() {});
navigator.mediaSession.setActionHandler('nexttrack', function() {}); navigator.mediaSession.setActionHandler('nexttrack', function() {});
} }
if (SettingsStore.getValue(UIFeature.Voip)) {
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
}
}
stop() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener('Call.incoming', this.onCallIncoming);
}
}
private onCallIncoming = (call) => {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
} }
getCallForRoom(roomId: string): MatrixCall { getCallForRoom(roomId: string): MatrixCall {
@ -262,6 +299,12 @@ export default class CallHandler {
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, { Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
title, description, title, description,
}); });
} else if (call.hangupReason === CallErrorCode.AnsweredElsewhere) {
this.play(AudioID.Busy);
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
title: _t("Answered Elsewhere"),
description: _t("The call was answered on another device."),
});
} else { } else {
this.play(AudioID.CallEnd); this.play(AudioID.CallEnd);
} }
@ -284,6 +327,11 @@ export default class CallHandler {
}); });
} }
private setCallAudioElement(call: MatrixCall) {
const audioElement = getRemoteAudioElement();
if (audioElement) call.setRemoteAudioElement(audioElement);
}
private setCallState(call: MatrixCall, status: CallState) { private setCallState(call: MatrixCall, status: CallState) {
console.log( console.log(
`Call state in ${call.roomId} changed to ${status}`, `Call state in ${call.roomId} changed to ${status}`,
@ -335,9 +383,12 @@ export default class CallHandler {
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement, localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
) { ) {
Analytics.trackEvent('voip', 'placeCall', 'type', type); Analytics.trackEvent('voip', 'placeCall', 'type', type);
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId); CountlyAnalytics.instance.trackStartCall(roomId, type === PlaceCallType.Video, false);
const call = createNewMatrixCall(MatrixClientPeg.get(), roomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);
this.setCallListeners(call); this.setCallListeners(call);
this.setCallAudioElement(call);
if (type === PlaceCallType.Voice) { if (type === PlaceCallType.Voice) {
call.placeVoiceCall(); call.placeVoiceCall();
} else if (type === 'video') { } else if (type === 'video') {
@ -413,6 +464,7 @@ export default class CallHandler {
case 'place_conference_call': case 'place_conference_call':
console.info("Place conference call in %s", payload.room_id); console.info("Place conference call in %s", payload.room_id);
Analytics.trackEvent('voip', 'placeConferenceCall'); Analytics.trackEvent('voip', 'placeConferenceCall');
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':
@ -442,6 +494,7 @@ export default class CallHandler {
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type); Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
this.calls.set(call.roomId, call) this.calls.set(call.roomId, call)
this.setCallListeners(call); this.setCallListeners(call);
this.setCallAudioElement(call);
} }
break; break;
case 'hangup': case 'hangup':
@ -456,16 +509,19 @@ export default class CallHandler {
} }
this.removeCallForRoom(payload.room_id); this.removeCallForRoom(payload.room_id);
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
} }
this.calls.get(payload.room_id).answer(); const call = this.calls.get(payload.room_id);
call.answer();
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
dis.dispatch({ dis.dispatch({
action: "view_room", action: "view_room",
room_id: payload.room_id, room_id: payload.room_id,
}); });
break; break;
}
} }
} }

View file

@ -31,6 +31,7 @@ import Spinner from "./components/views/elements/Spinner";
// Polyfill for Canvas.toBlob API using Canvas.toDataURL // Polyfill for Canvas.toBlob API using Canvas.toDataURL
import "blueimp-canvas-to-blob"; import "blueimp-canvas-to-blob";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import CountlyAnalytics from "./CountlyAnalytics";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -368,10 +369,13 @@ export default class ContentMessages {
private mediaConfig: IMediaConfig = null; private mediaConfig: IMediaConfig = null;
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) { sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => { const startTime = CountlyAnalytics.getTimestamp();
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e); console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
throw e; throw e;
}); });
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, {msgtype: "m.sticker"});
return prom;
} }
getUploadLimit() { getUploadLimit() {
@ -479,6 +483,7 @@ export default class ContentMessages {
} }
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) { private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
const startTime = CountlyAnalytics.getTimestamp();
const content: IContent = { const content: IContent = {
body: file.name || 'Attachment', body: file.name || 'Attachment',
info: { info: {
@ -563,7 +568,9 @@ export default class ContentMessages {
return promBefore; return promBefore;
}).then(function() { }).then(function() {
if (upload.canceled) throw new UploadCanceledError(); if (upload.canceled) throw new UploadCanceledError();
return matrixClient.sendMessage(roomId, content); const prom = matrixClient.sendMessage(roomId, content);
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, content);
return prom;
}, function(err) { }, function(err) {
error = err; error = err;
if (!upload.canceled) { if (!upload.canceled) {

973
src/CountlyAnalytics.ts Normal file
View file

@ -0,0 +1,973 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {randomString} from "matrix-js-sdk/src/randomstring";
import {getCurrentLanguage} from './languageHandler';
import PlatformPeg from './PlatformPeg';
import SdkConfig from './SdkConfig';
import {MatrixClientPeg} from "./MatrixClientPeg";
import {sleep} from "./utils/promise";
import RoomViewStore from "./stores/RoomViewStore";
// polyfill textencoder if necessary
import * as TextEncodingUtf8 from 'text-encoding-utf-8';
let TextEncoder = window.TextEncoder;
if (!TextEncoder) {
TextEncoder = TextEncodingUtf8.TextEncoder;
}
const INACTIVITY_TIME = 20; // seconds
const HEARTBEAT_INTERVAL = 5_000; // ms
const SESSION_UPDATE_INTERVAL = 60; // seconds
const MAX_PENDING_EVENTS = 1000;
enum Orientation {
Landscape = "landscape",
Portrait = "portrait",
}
/* eslint-disable camelcase */
interface IMetrics {
_resolution?: string;
_app_version?: string;
_density?: number;
_ua?: string;
_locale?: string;
}
interface IEvent {
key: string;
count: number;
sum?: number;
dur?: number;
segmentation?: Record<string, unknown>;
timestamp?: number; // TODO should we use the timestamp when we start or end for the event timestamp
hour?: unknown;
dow?: unknown;
}
interface IViewEvent extends IEvent {
key: "[CLY]_view";
}
interface IOrientationEvent extends IEvent {
key: "[CLY]_orientation";
segmentation: {
mode: Orientation;
};
}
interface IStarRatingEvent extends IEvent {
key: "[CLY]_star_rating";
segmentation: {
// we just care about collecting feedback, no need to associate with a feedback widget
widget_id?: string;
contactMe?: boolean;
email?: string;
rating: 1 | 2 | 3 | 4 | 5;
comment: string;
};
}
type Value = string | number | boolean;
interface IOperationInc {
"$inc": number;
}
interface IOperationMul {
"$mul": number;
}
interface IOperationMax {
"$max": number;
}
interface IOperationMin {
"$min": number;
}
interface IOperationSetOnce {
"$setOnce": Value;
}
interface IOperationPush {
"$push": Value | Value[];
}
interface IOperationAddToSet {
"$addToSet": Value | Value[];
}
interface IOperationPull {
"$pull": Value | Value[];
}
type Operation =
IOperationInc |
IOperationMul |
IOperationMax |
IOperationMin |
IOperationSetOnce |
IOperationPush |
IOperationAddToSet |
IOperationPull;
interface IUserDetails {
name?: string;
username?: string;
email?: string;
organization?: string;
phone?: string;
picture?: string;
gender?: string;
byear?: number;
custom?: Record<string, Value | Operation>; // `.` and `$` will be stripped out
}
interface ICrash {
_resolution?: string;
_app_version: string;
_ram_current?: number;
_ram_total?: number;
_disk_current?: number;
_disk_total?: number;
_orientation?: Orientation;
_online?: boolean;
_muted?: boolean;
_background?: boolean;
_view?: string;
_name?: string;
_error: string;
_nonfatal?: boolean;
_logs?: string;
_run?: number;
_custom?: Record<string, string>;
}
interface IParams {
// APP_KEY of an app for which to report
app_key: string;
// User identifier
device_id: string;
// Should provide value 1 to indicate session start
begin_session?: number;
// JSON object as string to provide metrics to track with the user
metrics?: string;
// Provides session duration in seconds, can be used as heartbeat to update current sessions duration, recommended time every 60 seconds
session_duration?: number;
// Should provide value 1 to indicate session end
end_session?: number;
// 10 digit UTC timestamp for recording past data.
timestamp?: number;
// current user local hour (0 - 23)
hour?: number;
// day of the week (0-sunday, 1 - monday, ... 6 - saturday)
dow?: number;
// JSON array as string containing event objects
events?: string; // IEvent[]
// JSON object as string containing information about users
user_details?: string;
// provide when changing device ID, so server would merge the data
old_device_id?: string;
// See ICrash
crash?: string;
}
interface IRoomSegments extends Record<string, Value> {
room_id: string; // hashed
num_users: number;
is_encrypted: boolean;
is_public: boolean;
}
interface ISendMessageEvent extends IEvent {
key: "send_message";
dur: number; // how long it to send (until remote echo)
segmentation: IRoomSegments & {
is_edit: boolean;
is_reply: boolean;
msgtype: string;
format?: string;
};
}
interface IRoomDirectoryEvent extends IEvent {
key: "room_directory";
}
interface IRoomDirectoryDoneEvent extends IEvent {
key: "room_directory_done";
dur: number; // time spent in the room directory modal
}
interface IRoomDirectorySearchEvent extends IEvent {
key: "room_directory_search";
sum: number; // number of search results
segmentation: {
query_length: number;
query_num_words: number;
};
}
interface IStartCallEvent extends IEvent {
key: "start_call";
segmentation: IRoomSegments & {
is_video: boolean;
is_jitsi: boolean;
};
}
interface IJoinCallEvent extends IEvent {
key: "join_call";
segmentation: IRoomSegments & {
is_video: boolean;
is_jitsi: boolean;
};
}
interface IBeginInviteEvent extends IEvent {
key: "begin_invite";
segmentation: IRoomSegments;
}
interface ISendInviteEvent extends IEvent {
key: "send_invite";
sum: number; // quantity that was invited
segmentation: IRoomSegments;
}
interface ICreateRoomEvent extends IEvent {
key: "create_room";
dur: number; // how long it took to create (until remote echo)
segmentation: {
room_id: string; // hashed
num_users: number;
is_encrypted: boolean;
is_public: boolean;
}
}
interface IJoinRoomEvent extends IEvent {
key: "join_room";
dur: number; // how long it took to join (until remote echo)
segmentation: {
room_id: string; // hashed
num_users: number;
is_encrypted: boolean;
is_public: boolean;
type: "room_directory" | "slash_command" | "link" | "invite";
};
}
/* eslint-enable camelcase */
const hashHex = async (input: string): Promise<string> => {
const buf = new TextEncoder().encode(input);
const digestBuf = await window.crypto.subtle.digest("sha-256", buf);
return [...new Uint8Array(digestBuf)].map((b: number) => b.toString(16).padStart(2, "0")).join("");
};
const knownScreens = new Set([
"register", "login", "forgot_password", "soft_logout", "new", "settings", "welcome", "home", "start", "directory",
"start_sso", "start_cas", "groups", "complete_security", "post_registration", "room", "user", "group",
]);
interface IViewData {
name: string;
url: string;
meta: Record<string, string>;
}
// Apply fn to all hash path parts after the 1st one
async function getViewData(anonymous = true): Promise<IViewData> {
const rand = randomString(8);
const { origin, hash } = window.location;
let { pathname } = window.location;
// Redact paths which could contain unexpected PII
if (origin.startsWith('file://')) {
pathname = `/<redacted_${rand}>/`; // XXX: inject rand because Count.ly doesn't like X->X transitions
}
let [_, screen, ...parts] = hash.split("/");
if (!knownScreens.has(screen)) {
screen = `<redacted_${rand}>`;
}
for (let i = 0; i < parts.length; i++) {
parts[i] = anonymous ? `<redacted_${rand}>` : await hashHex(parts[i]);
}
const hashStr = `${_}/${screen}/${parts.join("/")}`;
const url = origin + pathname + hashStr;
const meta = {};
let name = "$/" + hash;
switch (screen) {
case "room": {
name = "view_room";
const roomId = RoomViewStore.getRoomId();
name += " " + parts[0]; // XXX: workaround Count.ly missing X->X transitions
meta["room_id"] = parts[0];
Object.assign(meta, getRoomStats(roomId));
break;
}
}
return { name, url, meta };
}
const getRoomStats = (roomId: string) => {
const cli = MatrixClientPeg.get();
const room = cli?.getRoom(roomId);
return {
"num_users": room?.getJoinedMemberCount(),
"is_encrypted": cli?.isRoomEncrypted(roomId),
// eslint-disable-next-line camelcase
"is_public": room?.currentState.getStateEvents("m.room.join_rules", "")?.getContent()?.join_rule === "public",
}
}
// async wrapper for regex-powered String.prototype.replace
const strReplaceAsync = async (str: string, regex: RegExp, fn: (...args: string[]) => Promise<string>) => {
const promises: Promise<string>[] = [];
// dry-run to calculate the replace values
str.replace(regex, (...args: string[]) => {
promises.push(fn(...args));
return "";
});
const values = await Promise.all(promises);
return str.replace(regex, () => values.shift());
};
export default class CountlyAnalytics {
private baseUrl: URL = null;
private appKey: string = null;
private userKey: string = null;
private anonymous: boolean;
private appPlatform: string;
private appVersion = "unknown";
private initTime = CountlyAnalytics.getTimestamp();
private firstPage = true;
private heartbeatIntervalId: NodeJS.Timeout;
private activityIntervalId: NodeJS.Timeout;
private trackTime = true;
private lastBeat: number;
private storedDuration = 0;
private lastView: string;
private lastViewTime = 0;
private lastViewStoredDuration = 0;
private sessionStarted = false;
private heartbeatEnabled = false;
private inactivityCounter = 0;
private pendingEvents: IEvent[] = [];
private static internalInstance = new CountlyAnalytics();
public static get instance(): CountlyAnalytics {
return CountlyAnalytics.internalInstance;
}
public get disabled() {
return !this.baseUrl;
}
public canEnable() {
const config = SdkConfig.get();
return Boolean(navigator.doNotTrack !== "1" && config?.countly?.url && config?.countly?.appKey);
}
private async changeUserKey(userKey: string, merge = false) {
const oldUserKey = this.userKey;
this.userKey = userKey;
if (oldUserKey && merge) {
await this.request({ old_device_id: oldUserKey });
}
}
public async enable(anonymous = true) {
if (!this.disabled && this.anonymous === anonymous) return;
if (!this.canEnable()) return;
if (!this.disabled) {
// flush request queue as our userKey is going to change, no need to await it
this.request();
}
const config = SdkConfig.get();
this.baseUrl = new URL("/i", config.countly.url);
this.appKey = config.countly.appKey;
this.anonymous = anonymous;
if (anonymous) {
await this.changeUserKey(randomString(64))
} else {
await this.changeUserKey(await hashHex(MatrixClientPeg.get().getUserId()), true);
}
const platform = PlatformPeg.get();
this.appPlatform = platform.getHumanReadableName();
try {
this.appVersion = await platform.getAppVersion();
} catch (e) {
console.warn("Failed to get app version, using 'unknown'");
}
// start heartbeat
this.heartbeatIntervalId = setInterval(this.heartbeat.bind(this), HEARTBEAT_INTERVAL);
this.trackSessions();
this.trackErrors();
}
public async disable() {
if (this.disabled) return;
await this.track("Opt-Out" );
this.endSession();
window.clearInterval(this.heartbeatIntervalId);
window.clearTimeout(this.activityIntervalId)
this.baseUrl = null;
// remove listeners bound in trackSessions()
window.removeEventListener("beforeunload", this.endSession);
window.removeEventListener("unload", this.endSession);
window.removeEventListener("visibilitychange", this.onVisibilityChange);
window.removeEventListener("mousemove", this.onUserActivity);
window.removeEventListener("click", this.onUserActivity);
window.removeEventListener("keydown", this.onUserActivity);
window.removeEventListener("scroll", this.onUserActivity);
}
public reportFeedback(rating: 1 | 2 | 3 | 4 | 5, comment: string) {
this.track<IStarRatingEvent>("[CLY]_star_rating", { rating, comment }, null, {}, true);
}
public trackPageChange(generationTimeMs?: number) {
if (this.disabled) return;
// TODO use generationTimeMs
this.trackPageView();
}
private async trackPageView() {
this.reportViewDuration();
await sleep(0); // XXX: we sleep here because otherwise we get the old hash and not the new one
const viewData = await getViewData(this.anonymous);
const page = viewData.name;
this.lastView = page;
this.lastViewTime = CountlyAnalytics.getTimestamp();
const segments = {
...viewData.meta,
name: page,
visit: 1,
domain: window.location.hostname,
view: viewData.url,
segment: this.appPlatform,
start: this.firstPage,
};
if (this.firstPage) {
this.firstPage = false;
}
this.track<IViewEvent>("[CLY]_view", segments);
}
public static getTimestamp() {
return Math.floor(new Date().getTime() / 1000);
}
// store the last ms timestamp returned
// we do this to prevent the ts from ever decreasing in the case of system time changing
private lastMsTs = 0;
private getMsTimestamp() {
const ts = new Date().getTime();
if (this.lastMsTs >= ts) {
// increment ts as to keep our data points well-ordered
this.lastMsTs++;
} else {
this.lastMsTs = ts;
}
return this.lastMsTs;
}
public async recordError(err: Error | string, fatal = false) {
if (this.disabled || this.anonymous) return;
let error = "";
if (typeof err === "object") {
if (typeof err.stack !== "undefined") {
error = err.stack;
} else {
if (typeof err.name !== "undefined") {
error += err.name + ":";
}
if (typeof err.message !== "undefined") {
error += err.message + "\n";
}
if (typeof err.fileName !== "undefined") {
error += "in " + err.fileName + "\n";
}
if (typeof err.lineNumber !== "undefined") {
error += "on " + err.lineNumber;
}
if (typeof err.columnNumber !== "undefined") {
error += ":" + err.columnNumber;
}
}
} else {
error = err + "";
}
// sanitize the error from identifiers
error = await strReplaceAsync(error, /([!@+#]).+?:[\w:.]+/g, async (substring: string, glyph: string) => {
return glyph + await hashHex(substring.substring(1));
});
const metrics = this.getMetrics();
const ob: ICrash = {
_resolution: metrics?._resolution,
_error: error,
_app_version: this.appVersion,
_run: CountlyAnalytics.getTimestamp() - this.initTime,
_nonfatal: !fatal,
_view: this.lastView,
};
if (typeof navigator.onLine !== "undefined") {
ob._online = navigator.onLine;
}
ob._background = document.hasFocus();
this.request({ crash: JSON.stringify(ob) });
}
private trackErrors() {
//override global uncaught error handler
window.onerror = (msg, url, line, col, err) => {
if (typeof err !== "undefined") {
this.recordError(err, false);
} else {
let error = "";
if (typeof msg !== "undefined") {
error += msg + "\n";
}
if (typeof url !== "undefined") {
error += "at " + url;
}
if (typeof line !== "undefined") {
error += ":" + line;
}
if (typeof col !== "undefined") {
error += ":" + col;
}
error += "\n";
try {
const stack = [];
// eslint-disable-next-line no-caller
let f = arguments.callee.caller;
while (f) {
stack.push(f.name);
f = f.caller;
}
error += stack.join("\n");
} catch (ex) {
//silent error
}
this.recordError(error, false);
}
};
window.addEventListener('unhandledrejection', (event) => {
this.recordError(new Error(`Unhandled rejection (reason: ${event.reason?.stack || event.reason}).`), true);
});
}
private heartbeat() {
const args: Pick<IParams, "session_duration"> = {};
// extend session if needed
if (this.sessionStarted && this.trackTime) {
const last = CountlyAnalytics.getTimestamp();
if (last - this.lastBeat >= SESSION_UPDATE_INTERVAL) {
args.session_duration = last - this.lastBeat;
this.lastBeat = last;
}
}
// process event queue
if (this.pendingEvents.length > 0 || args.session_duration) {
this.request(args);
}
}
private async request(
args: Omit<IParams, "app_key" | "device_id" | "timestamp" | "hour" | "dow">
& Partial<Pick<IParams, "device_id">> = {},
) {
const request: IParams = {
app_key: this.appKey,
device_id: this.userKey,
...this.getTimeParams(),
...args,
};
if (this.pendingEvents.length > 0) {
const EVENT_BATCH_SIZE = 10;
const events = this.pendingEvents.splice(0, EVENT_BATCH_SIZE);
request.events = JSON.stringify(events);
}
const params = new URLSearchParams(request as {});
try {
await window.fetch(this.baseUrl.toString(), {
method: "POST",
mode: "no-cors",
cache: "no-cache",
redirect: "follow",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: params,
});
} catch (e) {
console.error("Analytics error: ", e);
}
}
private getTimeParams(): Pick<IParams, "timestamp" | "hour" | "dow"> {
const date = new Date();
return {
timestamp: this.getMsTimestamp(),
hour: date.getHours(),
dow: date.getDay(),
};
}
private queue(args: Omit<IEvent, "timestamp" | "hour" | "dow" | "count"> & Partial<Pick<IEvent, "count">>) {
const {count = 1, ...rest} = args;
const ev = {
...this.getTimeParams(),
...rest,
count,
platform: this.appPlatform,
app_version: this.appVersion,
}
this.pendingEvents.push(ev);
if (this.pendingEvents.length > MAX_PENDING_EVENTS) {
this.pendingEvents.shift();
}
}
private getOrientation = (): Orientation => {
return window.innerWidth > window.innerHeight ? Orientation.Landscape : Orientation.Portrait;
};
private reportOrientation = () => {
this.track<IOrientationEvent>("[CLY]_orientation", {
mode: this.getOrientation(),
});
};
private startTime() {
if (!this.trackTime) {
this.trackTime = true;
this.lastBeat = CountlyAnalytics.getTimestamp() - this.storedDuration;
this.lastViewTime = CountlyAnalytics.getTimestamp() - this.lastViewStoredDuration;
this.lastViewStoredDuration = 0;
}
}
private stopTime() {
if (this.trackTime) {
this.trackTime = false;
this.storedDuration = CountlyAnalytics.getTimestamp() - this.lastBeat;
this.lastViewStoredDuration = CountlyAnalytics.getTimestamp() - this.lastViewTime;
}
}
private getMetrics(): IMetrics {
if (this.anonymous) return undefined;
const metrics: IMetrics = {};
// getting app version
metrics._app_version = this.appVersion;
metrics._ua = navigator.userAgent;
// getting resolution
if (screen.width && screen.height) {
metrics._resolution = `${screen.width}x${screen.height}`;
}
// getting density ratio
if (window.devicePixelRatio) {
metrics._density = window.devicePixelRatio;
}
// getting locale
metrics._locale = getCurrentLanguage();
return metrics;
}
private async beginSession(heartbeat = true) {
if (!this.sessionStarted) {
this.reportOrientation();
window.addEventListener("resize", this.reportOrientation);
this.lastBeat = CountlyAnalytics.getTimestamp();
this.sessionStarted = true;
this.heartbeatEnabled = heartbeat;
const userDetails: IUserDetails = {
custom: {
"home_server": MatrixClientPeg.get() && MatrixClientPeg.getHomeserverName(), // TODO hash?
"anonymous": this.anonymous,
},
};
const request: Parameters<typeof CountlyAnalytics.prototype.request>[0] = {
begin_session: 1,
user_details: JSON.stringify(userDetails),
}
const metrics = this.getMetrics();
if (metrics) {
request.metrics = JSON.stringify(metrics);
}
await this.request(request);
}
}
private reportViewDuration() {
if (this.lastView) {
this.track<IViewEvent>("[CLY]_view", {
name: this.lastView,
}, null, {
dur: this.trackTime ? CountlyAnalytics.getTimestamp() - this.lastViewTime : this.lastViewStoredDuration,
});
this.lastView = null;
}
}
private endSession = () => {
if (this.sessionStarted) {
window.removeEventListener("resize", this.reportOrientation)
this.reportViewDuration();
this.request({
end_session: 1,
session_duration: CountlyAnalytics.getTimestamp() - this.lastBeat,
});
}
this.sessionStarted = false;
};
private onVisibilityChange = () => {
if (document.hidden) {
this.stopTime();
} else {
this.startTime();
}
};
private onUserActivity = () => {
if (this.inactivityCounter >= INACTIVITY_TIME) {
this.startTime();
}
this.inactivityCounter = 0;
};
private trackSessions() {
this.beginSession();
this.startTime();
window.addEventListener("beforeunload", this.endSession);
window.addEventListener("unload", this.endSession);
window.addEventListener("visibilitychange", this.onVisibilityChange);
window.addEventListener("mousemove", this.onUserActivity);
window.addEventListener("click", this.onUserActivity);
window.addEventListener("keydown", this.onUserActivity);
window.addEventListener("scroll", this.onUserActivity);
this.activityIntervalId = setInterval(() => {
this.inactivityCounter++;
if (this.inactivityCounter >= INACTIVITY_TIME) {
this.stopTime();
}
}, 60_000);
}
public trackBeginInvite(roomId: string) {
this.track<IBeginInviteEvent>("begin_invite", {}, roomId);
}
public trackSendInvite(startTime: number, roomId: string, qty: number) {
this.track<ISendInviteEvent>("send_invite", {}, roomId, {
dur: CountlyAnalytics.getTimestamp() - startTime,
sum: qty,
});
}
public async trackRoomCreate(startTime: number, roomId: string) {
if (this.disabled) return;
let endTime = CountlyAnalytics.getTimestamp();
const cli = MatrixClientPeg.get();
if (!cli.getRoom(roomId)) {
await new Promise(resolve => {
const handler = (room) => {
if (room.roomId === roomId) {
cli.off("Room", handler);
resolve();
}
};
cli.on("Room", handler);
});
endTime = CountlyAnalytics.getTimestamp();
}
this.track<ICreateRoomEvent>("create_room", {}, roomId, {
dur: endTime - startTime,
});
}
public trackRoomJoin(startTime: number, roomId: string, type: IJoinRoomEvent["segmentation"]["type"]) {
this.track<IJoinRoomEvent>("join_room", { type }, roomId, {
dur: CountlyAnalytics.getTimestamp() - startTime,
});
}
public async trackSendMessage(
startTime: number,
// eslint-disable-next-line camelcase
sendPromise: Promise<{event_id: string}>,
roomId: string,
isEdit: boolean,
isReply: boolean,
content: {format?: string, msgtype: string},
) {
if (this.disabled) return;
const cli = MatrixClientPeg.get();
const room = cli.getRoom(roomId);
const eventId = (await sendPromise).event_id;
let endTime = CountlyAnalytics.getTimestamp();
if (!room.findEventById(eventId)) {
await new Promise(resolve => {
const handler = (ev) => {
if (ev.getId() === eventId) {
room.off("Room.localEchoUpdated", handler);
resolve();
}
};
room.on("Room.localEchoUpdated", handler);
});
endTime = CountlyAnalytics.getTimestamp();
}
this.track<ISendMessageEvent>("send_message", {
is_edit: isEdit,
is_reply: isReply,
msgtype: content.msgtype,
format: content.format,
}, roomId, {
dur: endTime - startTime,
});
}
public trackStartCall(roomId: string, isVideo = false, isJitsi = false) {
this.track<IStartCallEvent>("start_call", {
is_video: isVideo,
is_jitsi: isJitsi,
}, roomId);
}
public trackJoinCall(roomId: string, isVideo = false, isJitsi = false) {
this.track<IJoinCallEvent>("join_call", {
is_video: isVideo,
is_jitsi: isJitsi,
}, roomId);
}
public trackRoomDirectoryBegin() {
this.track<IRoomDirectoryEvent>("room_directory");
}
public trackRoomDirectory(startTime: number) {
this.track<IRoomDirectoryDoneEvent>("room_directory_done", {}, null, {
dur: CountlyAnalytics.getTimestamp() - startTime,
});
}
public trackRoomDirectorySearch(numResults: number, query: string) {
this.track<IRoomDirectorySearchEvent>("room_directory_search", {
query_length: query.length,
query_num_words: query.split(" ").length,
}, null, {
sum: numResults,
});
}
public async track<E extends IEvent>(
key: E["key"],
segments?: Omit<E["segmentation"], "room_id" | "num_users" | "is_encrypted" | "is_public">,
roomId?: string,
args?: Partial<Pick<E, "dur" | "sum" | "timestamp">>,
anonymous = false,
) {
if (this.disabled && !anonymous) return;
let segmentation = segments || {};
if (roomId) {
segmentation = {
room_id: await hashHex(roomId),
...getRoomStats(roomId),
...segments,
};
}
this.queue({
key,
count: 1,
segmentation,
...args,
});
// if this event can be sent anonymously and we are disabled then dispatch it right away
if (this.disabled && anonymous) {
await this.request({ device_id: randomString(64) });
}
}
}
// expose on window for easy access from the console
window.mxCountlyAnalytics = CountlyAnalytics;

View file

@ -29,6 +29,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
import url from 'url'; import url from 'url';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import SettingsStore from './settings/SettingsStore';
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread"; import ReplyThread from "./components/views/elements/ReplyThread";
@ -171,7 +172,10 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag // Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
// because transformTags is used _before_ we filter by allowedSchemesByTag and // because transformTags is used _before_ we filter by allowedSchemesByTag and
// we don't want to allow images with `https?` `src`s. // we don't want to allow images with `https?` `src`s.
if (!attribs.src || !attribs.src.startsWith('mxc://')) { // We also drop inline images (as if they were not present at all) when the "show
// images" preference is disabled. Future work might expose some UI to reveal them
// like standalone image events have.
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
return { tagName, attribs: {}}; return { tagName, attribs: {}};
} }
attribs.src = MatrixClientPeg.get().mxcUrlToHttp( attribs.src = MatrixClientPeg.get().mxcUrlToHttp(

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2020 Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
/** /**
* Returns the actual height that an image of dimensions (fullWidth, fullHeight) * Returns the actual height that an image of dimensions (fullWidth, fullHeight)
* will occupy if resized to fit inside a thumbnail bounding box of size * will occupy if resized to fit inside a thumbnail bounding box of size
@ -30,11 +28,11 @@ limitations under the License.
* consume in the timeline, when performing scroll offset calcuations * consume in the timeline, when performing scroll offset calcuations
* (e.g. scroll locking) * (e.g. scroll locking)
*/ */
export function thumbHeight(fullWidth, fullHeight, thumbWidth, thumbHeight) { export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
if (!fullWidth || !fullHeight) { if (!fullWidth || !fullHeight) {
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even // Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
// log this because it's spammy // log this because it's spammy
return undefined; return null;
} }
if (fullWidth < thumbWidth && fullHeight < thumbHeight) { if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
// no scaling needs to be applied // no scaling needs to be applied

View file

@ -47,6 +47,8 @@ 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} from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import CountlyAnalytics from "./CountlyAnalytics";
import CallHandler from './CallHandler';
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";
@ -580,6 +582,10 @@ let _isLoggingOut = false;
*/ */
export function logout(): void { export function logout(): void {
if (!MatrixClientPeg.get()) return; if (!MatrixClientPeg.get()) return;
if (!CountlyAnalytics.instance.disabled) {
// user has logged out, fall back to anonymous
CountlyAnalytics.instance.enable(/* anonymous = */ true);
}
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
// logout doesn't work for guest sessions // logout doesn't work for guest sessions
@ -660,6 +666,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();
CallHandler.sharedInstance().start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting // Start Mjolnir even though we haven't checked the feature flag yet. Starting
// the thing just wastes CPU cycles, but should result in no actual functionality // the thing just wastes CPU cycles, but should result in no actual functionality
@ -755,6 +762,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
*/ */
export function stopMatrixClient(unsetClient = true): void { export function stopMatrixClient(unsetClient = true): void {
Notifier.stop(); Notifier.stop();
CallHandler.sharedInstance().stop();
UserActivity.sharedInstance().stop(); UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();

View file

@ -34,6 +34,7 @@ import * as StorageManager from './utils/StorageManager';
import IdentityAuthClient from './IdentityAuthClient'; import IdentityAuthClient from './IdentityAuthClient';
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager'; import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode"; import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
import SecurityCustomisations from "./customisations/Security";
export interface IMatrixClientCreds { export interface IMatrixClientCreds {
homeserverUrl: string; homeserverUrl: string;
@ -100,6 +101,12 @@ export interface IMatrixClientPeg {
*/ */
currentUserIsJustRegistered(): boolean; currentUserIsJustRegistered(): boolean;
/**
* If the current user has been registered by this device then this
* returns a boolean of whether it was within the last N hours given.
*/
userRegisteredWithinLastHours(hours: number): boolean;
/** /**
* Replace this MatrixClientPeg's client with a client instance that has * Replace this MatrixClientPeg's client with a client instance that has
* homeserver / identity server URLs and active credentials * homeserver / identity server URLs and active credentials
@ -150,6 +157,9 @@ class _MatrixClientPeg implements IMatrixClientPeg {
public setJustRegisteredUserId(uid: string): void { public setJustRegisteredUserId(uid: string): void {
this.justRegisteredUserId = uid; this.justRegisteredUserId = uid;
if (uid) {
window.localStorage.setItem("mx_registration_time", String(new Date().getTime()));
}
} }
public currentUserIsJustRegistered(): boolean { public currentUserIsJustRegistered(): boolean {
@ -159,6 +169,15 @@ class _MatrixClientPeg implements IMatrixClientPeg {
); );
} }
public userRegisteredWithinLastHours(hours: number): boolean {
try {
const date = new Date(window.localStorage.getItem("mx_registration_time"));
return ((new Date().getTime() - date.getTime()) / 36e5) <= hours;
} catch (e) {
return false;
}
}
public replaceUsingCreds(creds: IMatrixClientCreds): void { public replaceUsingCreds(creds: IMatrixClientCreds): void {
this.currentClientCreds = creds; this.currentClientCreds = creds;
this.createClient(creds); this.createClient(creds);
@ -273,7 +292,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
// These are always installed regardless of the labs flag so that // These are always installed regardless of the labs flag so that
// cross-signing features can toggle on without reloading and also be // cross-signing features can toggle on without reloading and also be
// accessed immediately after login. // accessed immediately after login.
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks); const customisedCallbacks = {
getDehydrationKey: SecurityCustomisations.getDehydrationKey,
};
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks);
this.matrixClient = createMatrixClient(opts); this.matrixClient = createMatrixClient(opts);

View file

@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
interface IModal<T extends any[]> { export interface IModal<T extends any[]> {
elem: React.ReactNode; elem: React.ReactNode;
className?: string; className?: string;
beforeClosePromise?: Promise<boolean>; beforeClosePromise?: Promise<boolean>;
@ -38,7 +38,7 @@ interface IModal<T extends any[]> {
close(...args: T): void; close(...args: T): void;
} }
interface IHandle<T extends any[]> { export interface IHandle<T extends any[]> {
finished: Promise<T>; finished: Promise<T>;
close(...args: T): void; close(...args: T): void;
} }
@ -147,6 +147,15 @@ export class ModalManager {
return this.appendDialogAsync<T>(...rest); return this.appendDialogAsync<T>(...rest);
} }
public closeCurrentModal(reason: string) {
const modal = this.getCurrentModal();
if (!modal) {
return;
}
modal.closeReason = reason;
modal.close();
}
private buildModal<T extends any[]>( private buildModal<T extends any[]>(
prom: Promise<React.ComponentType>, prom: Promise<React.ComponentType>,
props?: IProps<T>, props?: IProps<T>,

View file

@ -34,6 +34,8 @@ import SettingsStore from "./settings/SettingsStore";
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast"; import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
import {SettingLevel} from "./settings/SettingLevel"; import {SettingLevel} from "./settings/SettingLevel";
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers"; import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
import RoomViewStore from "./stores/RoomViewStore";
import UserActivity from "./UserActivity";
/* /*
* Dispatches: * Dispatches:
@ -376,6 +378,11 @@ export const Notifier = {
const room = MatrixClientPeg.get().getRoom(ev.getRoomId()); const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev); const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
if (actions && actions.notify) { if (actions && actions.notify) {
if (RoomViewStore.getRoomId() === room.roomId && UserActivity.sharedInstance().userActiveRecently()) {
// don't bother notifying as user was recently active in this room
return;
}
if (this.isEnabled()) { if (this.isEnabled()) {
this._displayPopupNotification(ev, room); this._displayPopupNotification(ev, room);
} }

View file

@ -40,11 +40,11 @@ export function inviteMultipleToRoom(roomId, addrs) {
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
} }
export function showStartChatInviteDialog() { export function showStartChatInviteDialog(initialText) {
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
Modal.createTrackedDialog( Modal.createTrackedDialog(
'Start DM', '', InviteDialog, {kind: KIND_DM}, 'Start DM', '', InviteDialog, {kind: KIND_DM, initialText},
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
); );
} }

View file

@ -21,6 +21,9 @@ import {MatrixClientPeg} from './MatrixClientPeg';
* if any. This could be the canonical alias if one exists, otherwise * if any. This could be the canonical alias if one exists, otherwise
* an alias selected arbitrarily but deterministically from the list * an alias selected arbitrarily but deterministically from the list
* of aliases. Otherwise return null; * of aliases. Otherwise return null;
*
* @param {Object} room The room object
* @returns {string} A display alias for the given room
*/ */
export function getDisplayAliasForRoom(room) { export function getDisplayAliasForRoom(room) {
return room.getCanonicalAlias() || room.getAltAliases()[0]; return room.getCanonicalAlias() || room.getAltAliases()[0];

View file

@ -50,8 +50,8 @@ class Skinner {
return null; return null;
} }
// components have to be functions. // components have to be functions or forwardRef objects with a render function.
const validType = typeof comp === 'function'; const validType = typeof comp === 'function' || comp.render;
if (!validType) { if (!validType) {
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`); throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
} }

View file

@ -47,6 +47,7 @@ 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 effects from "./components/views/elements/effects" import effects from "./components/views/elements/effects"
import CallHandler from "./CallHandler";
// 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 {
@ -519,6 +520,7 @@ export const Commands = [
action: 'view_room', action: 'view_room',
room_alias: roomAlias, room_alias: roomAlias,
auto_join: true, auto_join: true,
_type: "slash_command", // instrumentation
}); });
return success(); return success();
} else if (params[0][0] === '!') { } else if (params[0][0] === '!') {
@ -533,6 +535,7 @@ export const Commands = [
}, },
via_servers: viaServers, // for the rejoin button via_servers: viaServers, // for the rejoin button
auto_join: true, auto_join: true,
_type: "slash_command", // instrumentation
}); });
return success(); return success();
} else if (isPermalink) { } else if (isPermalink) {
@ -557,6 +560,7 @@ export const Commands = [
const dispatch = { const dispatch = {
action: 'view_room', action: 'view_room',
auto_join: true, auto_join: true,
_type: "slash_command", // instrumentation
}; };
if (entity[0] === '!') dispatch["room_id"] = entity; if (entity[0] === '!') dispatch["room_id"] = entity;
@ -1000,14 +1004,29 @@ export const Commands = [
description: _td("Opens chat with the given user"), description: _td("Opens chat with the given user"),
args: "<user-id>", args: "<user-id>",
runFn: function(roomId, userId) { runFn: function(roomId, userId) {
if (!userId || !userId.startsWith("@") || !userId.includes(":")) { // easter-egg for now: look up phone numbers through the thirdparty API
// (very dumb phone number detection...)
const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId);
if (!userId || (!userId.startsWith("@") || !userId.includes(":")) && !isPhoneNumber) {
return reject(this.getUsage()); return reject(this.getUsage());
} }
return success((async () => { return success((async () => {
if (isPhoneNumber) {
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
'm.id.phone': userId,
});
if (!results || results.length === 0 || !results[0].userid) {
throw new Error("Unable to find Matrix ID for phone number");
}
userId = results[0].userid;
}
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
dis.dispatch({ dis.dispatch({
action: 'view_room', action: 'view_room',
room_id: await ensureDMExists(MatrixClientPeg.get(), userId), room_id: roomId,
}); });
})()); })());
}, },
@ -1041,6 +1060,43 @@ export const Commands = [
}, },
category: CommandCategories.actions, category: CommandCategories.actions,
}), }),
new Command({
command: "holdcall",
description: _td("Places the call in the current room on hold"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
if (!call) {
return reject("No active call in this room");
}
call.setRemoteOnHold(true);
return success();
},
}),
new Command({
command: "unholdcall",
description: _td("Takes the call in the current room off hold"),
category: CommandCategories.other,
runFn: function(roomId, args) {
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
if (!call) {
return reject("No active call in this room");
}
call.setRemoteOnHold(false);
return success();
},
}),
// Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
new Command({
command: "me",
args: '<message>',
description: _td('Displays action'),
category: CommandCategories.messages,
hideCompletionAfterSpace: true,
}),
...effects.map((effect) => { ...effects.map((effect) => {
return new Command({ return new Command({
command: effect.command, command: effect.command,
@ -1064,16 +1120,6 @@ export const Commands = [
category: CommandCategories.effects, category: CommandCategories.effects,
}) })
}), }),
// Command definitions for autocompletion ONLY:
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
new Command({
command: "me",
args: '<message>',
description: _td('Displays action'),
category: CommandCategories.messages,
hideCompletionAfterSpace: true,
}),
]; ];
// build a map from names and aliases to the Command objects. // build a map from names and aliases to the Command objects.

View file

@ -455,7 +455,7 @@ function textForWidgetEvent(event) {
let widgetName = name || prevName || type || prevType || ''; let widgetName = name || prevName || type || prevType || '';
// Apply sentence case to widget name // Apply sentence case to widget name
if (widgetName && widgetName.length > 0) { if (widgetName && widgetName.length > 0) {
widgetName = widgetName[0].toUpperCase() + widgetName.slice(1) + ' '; widgetName = widgetName[0].toUpperCase() + widgetName.slice(1);
} }
// If the widget was removed, its content should be {}, but this is sufficiently // If the widget was removed, its content should be {}, but this is sufficiently

View file

@ -16,12 +16,14 @@ limitations under the License.
import {MatrixClientPeg} from "./MatrixClientPeg"; import {MatrixClientPeg} from "./MatrixClientPeg";
import shouldHideEvent from './shouldHideEvent'; import shouldHideEvent from './shouldHideEvent';
import * as sdk from "./index";
import {haveTileForEvent} from "./components/views/rooms/EventTile"; import {haveTileForEvent} from "./components/views/rooms/EventTile";
/** /**
* Returns true iff this event arriving in a room should affect the room's * Returns true iff this event arriving in a room should affect the room's
* count of unread messages * count of unread messages
*
* @param {Object} ev The event
* @returns {boolean} True if the given event should affect the unread message count
*/ */
export function eventTriggersUnreadCount(ev) { export function eventTriggersUnreadCount(ev) {
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {

View file

@ -257,6 +257,12 @@ const shortcuts: Record<Categories, IShortcut[]> = {
key: Key.SLASH, key: Key.SLASH,
}], }],
description: _td("Toggle this dialog"), description: _td("Toggle this dialog"),
}, {
keybinds: [{
modifiers: [CMD_OR_CTRL, Modifiers.ALT],
key: Key.H,
}],
description: _td("Go to Home View"),
}, },
], ],

View file

@ -205,7 +205,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
// onFocus should be called when the index gained focus in any manner // onFocus should be called when the index gained focus in any manner
// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}` // isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}`
// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition // ref should be passed to a DOM node which will be used for DOM compareDocumentPosition
export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] => { export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => {
const context = useContext(RovingTabIndexContext); const context = useContext(RovingTabIndexContext);
let ref = useRef<HTMLElement>(null); let ref = useRef<HTMLElement>(null);

View file

@ -470,6 +470,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
value={CREATE_STORAGE_OPTION_KEY} value={CREATE_STORAGE_OPTION_KEY}
name="keyPassphrase" name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY} checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
onChange={this._onKeyPassphraseChange}
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
@ -488,6 +489,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
value={CREATE_STORAGE_OPTION_PASSPHRASE} value={CREATE_STORAGE_OPTION_PASSPHRASE}
name="keyPassphrase" name="keyPassphrase"
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE} checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
onChange={this._onKeyPassphraseChange}
outlined outlined
> >
<div className="mx_CreateSecretStorageDialog_optionTitle"> <div className="mx_CreateSecretStorageDialog_optionTitle">
@ -509,7 +511,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
"Safeguard against losing access to encrypted messages & data by " + "Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.", "backing up encryption keys on your server.",
)}</p> )}</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup" onChange={this._onKeyPassphraseChange}> <div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{optionKey} {optionKey}
{optionPassphrase} {optionPassphrase}
</div> </div>

View file

@ -47,7 +47,7 @@ const LONG_DESC_PLACEHOLDER = _td(
some important <a href="foo">links</a> some important <a href="foo">links</a>
</p> </p>
<p> <p>
You can even use 'img' tags You can even add images with Matrix URLs <img src="mxc://url" />
</p> </p>
`); `);

View file

@ -15,20 +15,83 @@ limitations under the License.
*/ */
import * as React from "react"; import * as React from "react";
import {useContext, useState} from "react";
import AutoHideScrollbar from './AutoHideScrollbar'; import AutoHideScrollbar from './AutoHideScrollbar';
import { getHomePageUrl } from "../../utils/pages"; import {getHomePageUrl} from "../../utils/pages";
import { _t } from "../../languageHandler"; import {_t} from "../../languageHandler";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import * as sdk from "../../index"; import * as sdk from "../../index";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import BaseAvatar from "../views/avatars/BaseAvatar";
import {OwnProfileStore} from "../../stores/OwnProfileStore";
import AccessibleButton from "../views/elements/AccessibleButton";
import {UPDATE_EVENT} from "../../stores/AsyncStore";
import {useEventEmitter} from "../../hooks/useEventEmitter";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader";
import Analytics from "../../Analytics";
import CountlyAnalytics from "../../CountlyAnalytics";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); const onClickSendDm = () => {
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); Analytics.trackEvent('home_page', 'button', 'dm');
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'}); CountlyAnalytics.instance.track("home_page_button", { button: "dm" });
dis.dispatch({action: 'view_create_chat'});
};
const HomePage = () => { const onClickExplore = () => {
Analytics.trackEvent('home_page', 'button', 'room_directory');
CountlyAnalytics.instance.track("home_page_button", { button: "room_directory" });
dis.fire(Action.ViewRoomDirectory);
};
const onClickNewRoom = () => {
Analytics.trackEvent('home_page', 'button', 'create_room');
CountlyAnalytics.instance.track("home_page_button", { button: "create_room" });
dis.dispatch({action: 'view_create_room'});
};
interface IProps {
justRegistered?: boolean;
}
const getOwnProfile = (userId: string) => ({
displayName: OwnProfileStore.instance.displayName || userId,
avatarUrl: OwnProfileStore.instance.getHttpAvatarUrl(AVATAR_SIZE),
});
const UserWelcomeTop = () => {
const cli = useContext(MatrixClientContext);
const userId = cli.getUserId();
const [ownProfile, setOwnProfile] = useState(getOwnProfile(userId));
useEventEmitter(OwnProfileStore.instance, UPDATE_EVENT, () => {
setOwnProfile(getOwnProfile(userId));
});
return <div>
<MiniAvatarUploader
hasAvatar={!!ownProfile.avatarUrl}
hasAvatarLabel={_t("Great, that'll help people know it's you")}
noAvatarLabel={_t("Add a photo so people know it's you.")}
setAvatarUrl={url => cli.setAvatarUrl(url)}
>
<BaseAvatar
idName={userId}
name={ownProfile.displayName}
url={ownProfile.avatarUrl}
width={AVATAR_SIZE}
height={AVATAR_SIZE}
resizeMethod="crop"
/>
</MiniAvatarUploader>
<h1>{ _t("Welcome %(name)s", { name: ownProfile.displayName }) }</h1>
<h4>{ _t("Now, let's help you get started") }</h4>
</div>;
};
const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
const config = SdkConfig.get(); const config = SdkConfig.get();
const pageUrl = getHomePageUrl(config); const pageUrl = getHomePageUrl(config);
@ -37,18 +100,27 @@ const HomePage = () => {
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />; return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
} }
const brandingConfig = config.branding; let introSection;
let logoUrl = "themes/element/img/logos/element-logo.svg"; if (justRegistered) {
if (brandingConfig && brandingConfig.authHeaderLogoUrl) { introSection = <UserWelcomeTop />;
logoUrl = brandingConfig.authHeaderLogoUrl; } else {
const brandingConfig = config.branding;
let logoUrl = "themes/element/img/logos/element-logo.svg";
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
logoUrl = brandingConfig.authHeaderLogoUrl;
}
introSection = <React.Fragment>
<img src={logoUrl} alt={config.brand} />
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand }) }</h1>
<h4>{ _t("Liberate your communication") }</h4>
</React.Fragment>;
} }
const AccessibleButton = sdk.getComponent("elements.AccessibleButton");
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default"> return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper"> <div className="mx_HomePage_default_wrapper">
<img src={logoUrl} alt={config.brand || "Element"} /> { introSection }
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Element" }) }</h1>
<h4>{ _t("Liberate your communication") }</h4>
<div className="mx_HomePage_default_buttons"> <div className="mx_HomePage_default_buttons">
<AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm"> <AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm">
{ _t("Send a Direct Message") } { _t("Send a Direct Message") }

View file

@ -38,6 +38,7 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { MatrixClientPeg } from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomListNumResults from "../views/rooms/RoomListNumResults"; import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -142,7 +143,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const bottomEdge = list.offsetHeight + list.scrollTop; const bottomEdge = list.offsetHeight + list.scrollTop;
const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist"); const sublists = list.querySelectorAll<HTMLDivElement>(".mx_RoomSublist");
const headerRightMargin = 16; // calculated from margins and widths to align with non-sticky tiles const headerRightMargin = 15; // calculated from margins and widths to align with non-sticky tiles
const headerStickyWidth = list.clientWidth - headerRightMargin; const headerStickyWidth = list.clientWidth - headerRightMargin;
// We track which styles we want on a target before making the changes to avoid // We track which styles we want on a target before making the changes to avoid
@ -213,10 +214,19 @@ export default class LeftPanel extends React.Component<IProps, IState> {
if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { if (!header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.add("mx_RoomSublist_headerContainer_stickyBottom"); header.classList.add("mx_RoomSublist_headerContainer_stickyBottom");
} }
const offset = window.innerHeight - (list.parentElement.offsetTop + list.parentElement.offsetHeight);
const newBottom = `${offset}px`;
if (header.style.bottom !== newBottom) {
header.style.bottom = newBottom;
}
} else { } else {
if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) { if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom"); header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom");
} }
if (header.style.bottom) {
header.style.removeProperty('bottom');
}
} }
if (style.stickyTop || style.stickyBottom) { if (style.stickyTop || style.stickyBottom) {
@ -425,6 +435,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
{roomList} {roomList}
</div> </div>
</div> </div>
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
</aside> </aside>
</div> </div>
); );

View file

@ -0,0 +1,149 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {useContext, useEffect, useMemo} from "react";
import {Resizable} from "re-resizable";
import classNames from "classnames";
import AccessibleButton from "../views/elements/AccessibleButton";
import {useRovingTabIndex} from "../../accessibility/RovingTabIndex";
import {Key} from "../../Keyboard";
import {useLocalStorageState} from "../../hooks/useLocalStorageState";
import MatrixClientContext from "../../contexts/MatrixClientContext";
import WidgetUtils, {IWidgetEvent} from "../../utils/WidgetUtils";
import {useAccountData} from "../../hooks/useAccountData";
import AppTile from "../views/elements/AppTile";
import {useSettingValue} from "../../hooks/useSettings";
interface IProps {
onResize(): void;
}
const MIN_HEIGHT = 100;
const MAX_HEIGHT = 500; // or 50% of the window height
const INITIAL_HEIGHT = 280;
const LeftPanelWidget: React.FC<IProps> = ({ onResize }) => {
const cli = useContext(MatrixClientContext);
const mWidgetsEvent = useAccountData<Record<string, IWidgetEvent>>(cli, "m.widgets");
const leftPanelWidgetId = useSettingValue("Widgets.leftPanel");
const app = useMemo(() => {
if (!mWidgetsEvent || !leftPanelWidgetId) return null;
const widgetConfig = Object.values(mWidgetsEvent).find(w => w.id === leftPanelWidgetId);
if (!widgetConfig) return null;
return WidgetUtils.makeAppConfig(
widgetConfig.state_key,
widgetConfig.content,
widgetConfig.sender,
null,
widgetConfig.id);
}, [mWidgetsEvent, leftPanelWidgetId]);
const [height, setHeight] = useLocalStorageState("left-panel-widget-height", INITIAL_HEIGHT);
const [expanded, setExpanded] = useLocalStorageState("left-panel-widget-expanded", true);
useEffect(onResize, [expanded]);
const [onFocus, isActive, ref] = useRovingTabIndex();
const tabIndex = isActive ? 0 : -1;
if (!app) return null;
let content;
if (expanded) {
content = <Resizable
size={{height} as any}
minHeight={MIN_HEIGHT}
maxHeight={Math.min(window.innerHeight / 2, MAX_HEIGHT)}
onResize={onResize}
onResizeStop={(e, dir, ref, d) => {
setHeight(height + d.height);
}}
handleWrapperClass="mx_LeftPanelWidget_resizerHandles"
handleClasses={{top: "mx_LeftPanelWidget_resizerHandle"}}
className="mx_LeftPanelWidget_resizeBox"
enable={{ top: true }}
>
<AppTile
app={app}
fullWidth
show
showMenubar={false}
userWidget
userId={cli.getUserId()}
creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad}
/>
</Resizable>;
}
return <div className="mx_LeftPanelWidget">
<div
onFocus={onFocus}
className="mx_LeftPanelWidget_headerContainer"
onKeyDown={(ev: React.KeyboardEvent) => {
switch (ev.key) {
case Key.ARROW_LEFT:
ev.stopPropagation();
setExpanded(false);
break;
case Key.ARROW_RIGHT: {
ev.stopPropagation();
setExpanded(true);
break;
}
}
}}
>
<div className="mx_LeftPanelWidget_stickable">
<AccessibleButton
onFocus={onFocus}
inputRef={ref}
tabIndex={tabIndex}
className="mx_LeftPanelWidget_headerText"
role="treeitem"
aria-expanded={expanded}
aria-level={1}
onClick={() => {
setExpanded(e => !e);
}}
>
<span className={classNames({
"mx_LeftPanelWidget_collapseBtn": true,
"mx_LeftPanelWidget_collapseBtn_collapsed": !expanded,
})} />
<span>{ WidgetUtils.getWidgetName(app) }</span>
</AccessibleButton>
{/* Code for the maximise button for once we have full screen widgets */}
{/*<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={() => {
}}
className="mx_LeftPanelWidget_maximizeButton"
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
title={_t("Maximize")}
/>*/}
</div>
</div>
{ content }
</div>;
};
export default LeftPanelWidget;

View file

@ -21,7 +21,7 @@ import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { DragDropContext } from 'react-beautiful-dnd'; import { DragDropContext } from 'react-beautiful-dnd';
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard'; import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isMac} from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import CallMediaHandler from '../../CallMediaHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
import NonUrgentToastContainer from "./NonUrgentToastContainer"; import NonUrgentToastContainer from "./NonUrgentToastContainer";
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload"; import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import Modal from "../../Modal";
import { ICollapseConfig } from "../../resizer/distributors/collapse"; import { ICollapseConfig } from "../../resizer/distributors/collapse";
// 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)
@ -88,6 +89,7 @@ interface IProps {
currentUserId?: string; currentUserId?: string;
currentGroupId?: string; currentGroupId?: string;
currentGroupIsNew?: boolean; currentGroupIsNew?: boolean;
justRegistered?: boolean;
} }
interface IUsageLimit { interface IUsageLimit {
@ -391,6 +393,7 @@ class LoggedInView extends React.Component<IProps, IState> {
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev); const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey; const hasModifier = ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey;
const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT; const isModifier = ev.key === Key.ALT || ev.key === Key.CONTROL || ev.key === Key.META || ev.key === Key.SHIFT;
const modKey = isMac ? ev.metaKey : ev.ctrlKey;
switch (ev.key) { switch (ev.key) {
case Key.PAGE_UP: case Key.PAGE_UP:
@ -435,6 +438,16 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
break; break;
case Key.H:
if (ev.altKey && modKey) {
dis.dispatch({
action: 'view_home_page',
});
Modal.closeCurrentModal("homeKeyboardShortcut");
handled = true;
}
break;
case Key.ARROW_UP: case Key.ARROW_UP:
case Key.ARROW_DOWN: case Key.ARROW_DOWN:
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) { if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
@ -573,7 +586,7 @@ class LoggedInView extends React.Component<IProps, IState> {
break; break;
case PageTypes.HomePage: case PageTypes.HomePage:
pageElement = <HomePage />; pageElement = <HomePage justRegistered={this.props.justRegistered} />;
break; break;
case PageTypes.UserView: case PageTypes.UserView:

View file

@ -29,6 +29,7 @@ import 'focus-visible';
import 'what-input'; import 'what-input';
import Analytics from "../../Analytics"; import Analytics from "../../Analytics";
import CountlyAnalytics from "../../CountlyAnalytics";
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker"; import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg"; import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
import PlatformPeg from "../../PlatformPeg"; import PlatformPeg from "../../PlatformPeg";
@ -61,7 +62,7 @@ import DMRoomMap from '../../utils/DMRoomMap';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher'; import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache'; import { storeRoomAliasInCache } from '../../RoomAliasCache';
import { defer, IDeferred } from "../../utils/promise"; import { defer, IDeferred, sleep } from "../../utils/promise";
import ToastStore from "../../stores/ToastStore"; import ToastStore from "../../stores/ToastStore";
import * as StorageManager from "../../utils/StorageManager"; import * as StorageManager from "../../utils/StorageManager";
import type LoggedInViewType from "./LoggedInView"; import type LoggedInViewType from "./LoggedInView";
@ -86,37 +87,37 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
export enum Views { export enum Views {
// a special initial state which is only used at startup, while we are // a special initial state which is only used at startup, while we are
// trying to re-animate a matrix client or register as a guest. // trying to re-animate a matrix client or register as a guest.
LOADING = 0, LOADING,
// we are showing the welcome view // we are showing the welcome view
WELCOME = 1, WELCOME,
// we are showing the login view // we are showing the login view
LOGIN = 2, LOGIN,
// we are showing the registration view // we are showing the registration view
REGISTER = 3, REGISTER,
// completing the registration flow
POST_REGISTRATION = 4,
// showing the 'forgot password' view // showing the 'forgot password' view
FORGOT_PASSWORD = 5, FORGOT_PASSWORD,
// showing flow to trust this new device with cross-signing // showing flow to trust this new device with cross-signing
COMPLETE_SECURITY = 6, COMPLETE_SECURITY,
// flow to setup SSSS / cross-signing on this account // flow to setup SSSS / cross-signing on this account
E2E_SETUP = 7, E2E_SETUP,
// we are logged in with an active matrix client. // we are logged in with an active matrix client. The logged_in state also
LOGGED_IN = 8, // includes guests users as they too are logged in at the client level.
LOGGED_IN,
// We are logged out (invalid token) but have our local state again. The user // We are logged out (invalid token) but have our local state again. The user
// should log back in to rehydrate the client. // should log back in to rehydrate the client.
SOFT_LOGOUT = 9, SOFT_LOGOUT,
} }
const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas"];
// Actions that are redirected through the onboarding process prior to being // Actions that are redirected through the onboarding process prior to being
// re-dispatched. NOTE: some actions are non-trivial and would require // re-dispatched. NOTE: some actions are non-trivial and would require
// re-factoring to be included in this list in future. // re-factoring to be included in this list in future.
@ -199,6 +200,7 @@ interface IState {
roomOobData?: object; roomOobData?: object;
viaServers?: string[]; viaServers?: string[];
pendingInitialSync?: boolean; pendingInitialSync?: boolean;
justRegistered?: boolean;
} }
export default class MatrixChat extends React.PureComponent<IProps, IState> { export default class MatrixChat extends React.PureComponent<IProps, IState> {
@ -349,6 +351,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (SettingsStore.getValue("analyticsOptIn")) { if (SettingsStore.getValue("analyticsOptIn")) {
Analytics.enable(); Analytics.enable();
} }
CountlyAnalytics.instance.enable(/* anonymous = */ true);
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle stage // TODO: [REACT-WARNING] Replace with appropriate lifecycle stage
@ -363,6 +366,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (this.shouldTrackPageChange(prevState, this.state)) { if (this.shouldTrackPageChange(prevState, this.state)) {
const durationMs = this.stopPageChangeTimer(); const durationMs = this.stopPageChangeTimer();
Analytics.trackPageChange(durationMs); Analytics.trackPageChange(durationMs);
CountlyAnalytics.instance.trackPageChange(durationMs);
} }
if (this.focusComposer) { if (this.focusComposer) {
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
@ -415,6 +419,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else { } else {
dis.dispatch({action: "view_welcome_page"}); dis.dispatch({action: "view_welcome_page"});
} }
} else if (SettingsStore.getValue("analyticsOptIn")) {
CountlyAnalytics.instance.enable(/* anonymous = */ false);
} }
}); });
// Note we don't catch errors from this: we catch everything within // Note we don't catch errors from this: we catch everything within
@ -473,6 +479,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
const newState = { const newState = {
currentUserId: null, currentUserId: null,
justRegistered: false,
}; };
Object.assign(newState, state); Object.assign(newState, state);
this.setState(newState); this.setState(newState);
@ -554,11 +561,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
ThemeController.isLogin = true; ThemeController.isLogin = true;
this.themeWatcher.recheck(); this.themeWatcher.recheck();
break; break;
case 'start_post_registration':
this.setState({
view: Views.POST_REGISTRATION,
});
break;
case 'start_password_recovery': case 'start_password_recovery':
this.setStateForNewView({ this.setStateForNewView({
view: Views.FORGOT_PASSWORD, view: Views.FORGOT_PASSWORD,
@ -645,8 +647,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
case Action.ViewRoomDirectory: { case Action.ViewRoomDirectory: {
const RoomDirectory = sdk.getComponent("structures.RoomDirectory"); const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {}, Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
'mx_RoomDirectory_dialogWrapper', false, true); initialText: payload.initialText,
}, 'mx_RoomDirectory_dialogWrapper', false, true);
// View the welcome or home page if we need something to look at // View the welcome or home page if we need something to look at
this.viewSomethingBehindModal(); this.viewSomethingBehindModal();
@ -663,13 +666,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.viewWelcome(); this.viewWelcome();
break; break;
case 'view_home_page': case 'view_home_page':
this.viewHome(); this.viewHome(payload.justRegistered);
break; break;
case 'view_start_chat_or_reuse': case 'view_start_chat_or_reuse':
this.chatCreateOrReuse(payload.user_id); this.chatCreateOrReuse(payload.user_id);
break; break;
case 'view_create_chat': case 'view_create_chat':
showStartChatInviteDialog(); showStartChatInviteDialog(payload.initialText || "");
break; break;
case 'view_invite': case 'view_invite':
showRoomInviteDialog(payload.roomId); showRoomInviteDialog(payload.roomId);
@ -750,7 +753,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, true);
SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false); SettingsStore.setValue("showCookieBar", null, SettingLevel.DEVICE, false);
hideAnalyticsToast(); hideAnalyticsToast();
Analytics.enable(); if (Analytics.canEnable()) {
Analytics.enable();
}
if (CountlyAnalytics.instance.canEnable()) {
CountlyAnalytics.instance.enable(/* anonymous = */ false);
}
break; break;
case 'reject_cookies': case 'reject_cookies':
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false); SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
@ -942,10 +950,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.themeWatcher.recheck(); this.themeWatcher.recheck();
} }
private viewHome() { private viewHome(justRegistered = false) {
// The home page requires the "logged in" view, so we'll set that. // The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({ this.setStateForNewView({
view: Views.LOGGED_IN, view: Views.LOGGED_IN,
justRegistered,
}); });
this.setPage(PageTypes.HomePage); this.setPage(PageTypes.HomePage);
this.notifyNewScreen('home'); this.notifyNewScreen('home');
@ -1179,7 +1188,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
if (welcomeUserRoom === null) { if (welcomeUserRoom === null) {
// We didn't redirect to the welcome user room, so show // We didn't redirect to the welcome user room, so show
// the homepage. // the homepage.
dis.dispatch({action: 'view_home_page'}); dis.dispatch({action: 'view_home_page', justRegistered: true});
} }
} else if (ThreepidInviteStore.instance.pickBestInvite()) { } else if (ThreepidInviteStore.instance.pickBestInvite()) {
// The user has a 3pid invite pending - show them that // The user has a 3pid invite pending - show them that
@ -1192,7 +1201,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} else { } else {
// The user has just logged in after registering, // The user has just logged in after registering,
// so show the homepage. // so show the homepage.
dis.dispatch({action: 'view_home_page'}); dis.dispatch({action: 'view_home_page', justRegistered: true});
} }
} else { } else {
this.showScreenAfterLogin(); this.showScreenAfterLogin();
@ -1200,7 +1209,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
StorageManager.tryPersistStorage(); StorageManager.tryPersistStorage();
if (SettingsStore.getValue("showCookieBar") && Analytics.canEnable()) { // defer the following actions by 30 seconds to not throw them at the user immediately
await sleep(30);
if (SettingsStore.getValue("showCookieBar") &&
(Analytics.canEnable() || CountlyAnalytics.instance.canEnable())
) {
showAnalyticsToast(this.props.config.piwik?.policyUrl); showAnalyticsToast(this.props.config.piwik?.policyUrl);
} }
} }
@ -1330,8 +1343,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.firstSyncComplete = true; this.firstSyncComplete = true;
this.firstSyncPromise.resolve(); this.firstSyncPromise.resolve();
if (Notifier.shouldShowPrompt()) { if (Notifier.shouldShowPrompt() && !MatrixClientPeg.userRegisteredWithinLastHours(24)) {
showNotificationsToast(); showNotificationsToast(false);
} }
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
@ -1340,18 +1353,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
}); });
if (SettingsStore.getValue(UIFeature.Voip)) {
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
}
cli.on('Session.logged_out', function(errObj) { cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return; if (Lifecycle.isLoggingOut()) return;
@ -1394,6 +1395,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
const dft = new DecryptionFailureTracker((total, errorCode) => { const dft = new DecryptionFailureTracker((total, errorCode) => {
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
}, (errorCode) => { }, (errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation // Map JS-SDK error codes to tracker codes for aggregation
switch (errorCode) { switch (errorCode) {
@ -1535,6 +1537,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
} }
showScreen(screen: string, params?: {[key: string]: any}) { showScreen(screen: string, params?: {[key: string]: any}) {
const cli = MatrixClientPeg.get();
const isLoggedOutOrGuest = !cli || cli.isGuest();
if (!isLoggedOutOrGuest && AUTH_SCREENS.includes(screen)) {
// user is logged in and landing on an auth page which will uproot their session, redirect them home instead
dis.dispatch({ action: "view_home_page" });
return;
}
if (screen === 'register') { if (screen === 'register') {
dis.dispatch({ dis.dispatch({
action: 'start_registration', action: 'start_registration',
@ -1551,7 +1561,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
params: params, params: params,
}); });
} else if (screen === 'soft_logout') { } else if (screen === 'soft_logout') {
if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId() && !Lifecycle.isSoftLogout()) { if (cli.getUserId() && !Lifecycle.isSoftLogout()) {
// Logged in - visit a room // Logged in - visit a room
this.viewLastRoom(); this.viewLastRoom();
} else { } else {
@ -1581,6 +1591,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
action: 'require_registration', action: 'require_registration',
}); });
} else if (screen === 'directory') { } else if (screen === 'directory') {
if (this.state.view === Views.WELCOME) {
CountlyAnalytics.instance.track("onboarding_room_directory");
}
dis.fire(Action.ViewRoomDirectory); dis.fire(Action.ViewRoomDirectory);
} else if (screen === "start_sso" || screen === "start_cas") { } else if (screen === "start_sso" || screen === "start_cas") {
// TODO if logged in, skip SSO // TODO if logged in, skip SSO
@ -1599,14 +1612,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
dis.dispatch({ dis.dispatch({
action: 'view_my_groups', action: 'view_my_groups',
}); });
} else if (screen === 'complete_security') {
dis.dispatch({
action: 'start_complete_security',
});
} else if (screen === 'post_registration') {
dis.dispatch({
action: 'start_post_registration',
});
} else if (screen.indexOf('room/') === 0) { } else if (screen.indexOf('room/') === 0) {
// Rooms can have the following formats: // Rooms can have the following formats:
// #room_alias:domain or !opaque_id:domain // #room_alias:domain or !opaque_id:domain
@ -1777,14 +1782,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
return Lifecycle.setLoggedIn(credentials); return Lifecycle.setLoggedIn(credentials);
} }
onFinishPostRegistration = () => {
// Don't confuse this with "PageType" which is the middle window to show
this.setState({
view: Views.LOGGED_IN,
});
this.showScreen("settings");
};
onSendEvent(roomId: string, event: MatrixEvent) { onSendEvent(roomId: string, event: MatrixEvent) {
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
if (!cli) { if (!cli) {
@ -1949,13 +1946,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
accountPassword={this.accountPassword} accountPassword={this.accountPassword}
/> />
); );
} else if (this.state.view === Views.POST_REGISTRATION) {
// needs to be before normal PageTypes as you are logged in technically
const PostRegistration = sdk.getComponent('structures.auth.PostRegistration');
view = (
<PostRegistration
onComplete={this.onFinishPostRegistration} />
);
} else if (this.state.view === Views.LOGGED_IN) { } else if (this.state.view === Views.LOGGED_IN) {
// store errors stop the client syncing and require user intervention, so we'll // store errors stop the client syncing and require user intervention, so we'll
// be showing a dialog. Don't show anything else. // be showing a dialog. Don't show anything else.

View file

@ -30,6 +30,8 @@ import {_t} from "../../languageHandler";
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {textForEvent} from "../../TextForEvent"; import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message']; const continuedTypes = ['m.sticker', 'm.room.message'];
@ -952,15 +954,25 @@ class CreationGrouper {
}).reduce((a, b) => a.concat(b), []); }).reduce((a, b) => a.concat(b), []);
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one // Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
const ev = this.events[this.events.length - 1]; const ev = this.events[this.events.length - 1];
let summaryText;
const roomId = ev.getRoomId();
const creator = ev.sender ? ev.sender.name : ev.getSender();
if (DMRoomMap.shared().getUserIdForRoomId(roomId)) {
summaryText = _t("%(creator)s created this DM.", { creator });
} else {
summaryText = _t("%(creator)s created and configured the room.", { creator });
}
ret.push(<NewRoomIntro key="newroomintro" />);
ret.push( ret.push(
<EventListSummary <EventListSummary
key="roomcreationsummary" key="roomcreationsummary"
events={this.events} events={this.events}
onToggle={panel._onHeightChanged} // Update scroll state onToggle={panel._onHeightChanged} // Update scroll state
summaryMembers={[ev.sender]} summaryMembers={[ev.sender]}
summaryText={_t("%(creator)s created and configured the room.", { summaryText={summaryText}
creator: ev.sender ? ev.sender.name : ev.getSender(),
})}
> >
{ eventTiles } { eventTiles }
</EventListSummary>, </EventListSummary>,

View file

@ -33,6 +33,7 @@ import SettingsStore from "../../settings/SettingsStore";
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore"; import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore"; import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore"; import FlairStore from "../../stores/FlairStore";
import CountlyAnalytics from "../../CountlyAnalytics";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
@ -43,12 +44,16 @@ function track(action) {
export default class RoomDirectory extends React.Component { export default class RoomDirectory extends React.Component {
static propTypes = { static propTypes = {
initialText: PropTypes.string,
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };
constructor(props) { constructor(props) {
super(props); super(props);
CountlyAnalytics.instance.trackRoomDirectoryBegin();
this.startTime = CountlyAnalytics.getTimestamp();
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0]; const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
this.state = { this.state = {
publicRooms: [], publicRooms: [],
@ -57,7 +62,7 @@ export default class RoomDirectory extends React.Component {
error: null, error: null,
instanceId: undefined, instanceId: undefined,
roomServer: MatrixClientPeg.getHomeserverName(), roomServer: MatrixClientPeg.getHomeserverName(),
filterString: null, filterString: this.props.initialText || "",
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes") selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
? selectedCommunityId ? selectedCommunityId
: null, : null,
@ -198,6 +203,11 @@ export default class RoomDirectory extends React.Component {
return; return;
} }
if (this.state.filterString) {
const count = data.total_room_count_estimate || data.chunk.length;
CountlyAnalytics.instance.trackRoomDirectorySearch(count, this.state.filterString);
}
this.nextBatch = data.next_batch; this.nextBatch = data.next_batch;
this.setState((s) => { this.setState((s) => {
s.publicRooms.push(...(data.chunk || [])); s.publicRooms.push(...(data.chunk || []));
@ -407,7 +417,7 @@ export default class RoomDirectory extends React.Component {
}; };
onCreateRoomClick = room => { onCreateRoomClick = room => {
this.props.onFinished(); this.onFinished();
dis.dispatch({ dis.dispatch({
action: 'view_create_room', action: 'view_create_room',
public: true, public: true,
@ -419,11 +429,12 @@ export default class RoomDirectory extends React.Component {
} }
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) { showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
this.props.onFinished(); this.onFinished();
const payload = { const payload = {
action: 'view_room', action: 'view_room',
auto_join: autoJoin, auto_join: autoJoin,
should_peek: shouldPeek, should_peek: shouldPeek,
_type: "room_directory", // instrumentation
}; };
if (room) { if (room) {
// Don't let the user view a room they won't be able to either // Don't let the user view a room they won't be able to either
@ -575,6 +586,11 @@ export default class RoomDirectory extends React.Component {
} }
}; };
onFinished = () => {
CountlyAnalytics.instance.trackRoomDirectory(this.startTime);
this.props.onFinished();
};
render() { render() {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
@ -671,6 +687,7 @@ export default class RoomDirectory extends React.Component {
onJoinClick={this.onJoinFromSearchClick} onJoinClick={this.onJoinFromSearchClick}
placeholder={placeholder} placeholder={placeholder}
showJoinButton={showJoinButton} showJoinButton={showJoinButton}
initialText={this.props.initialText}
/> />
{dropdown} {dropdown}
</div>; </div>;
@ -693,7 +710,7 @@ export default class RoomDirectory extends React.Component {
<BaseDialog <BaseDialog
className={'mx_RoomDirectory_dialog'} className={'mx_RoomDirectory_dialog'}
hasCancel={true} hasCancel={true}
onFinished={this.props.onFinished} onFinished={this.onFinished}
title={title} title={title}
> >
<div className="mx_RoomDirectory"> <div className="mx_RoomDirectory">

View file

@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
onBlur={this.onBlur} onBlur={this.onBlur}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}
placeholder={_t("Search")} placeholder={_t("Filter")}
autoComplete="off" autoComplete="off"
/> />
); );
@ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
if (this.props.isMinimized) { if (this.props.isMinimized) {
icon = ( icon = (
<AccessibleButton <AccessibleButton
title={_t("Search rooms")} title={_t("Filter rooms and people")}
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle" className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
onClick={this.openSearch} onClick={this.openSearch}
/> />

View file

@ -18,13 +18,11 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import { _t, _td } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import { CallState, CallType } from 'matrix-js-sdk/lib/webrtc/call';
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -41,16 +39,6 @@ export default class RoomStatusBar extends React.Component {
static propTypes = { static propTypes = {
// the room this statusbar is representing. // the room this statusbar is representing.
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
// This is true when the user is alone in the room, but has also sent a message.
// Used to suggest to the user to invite someone
sentMessageAndIsAlone: PropTypes.bool,
// The active call in the room, if any (means we show the call bar
// along with the status of the call)
callState: PropTypes.string,
// The type of the call in progress, or null if no call is in progress
callType: PropTypes.string,
// true if the room is being peeked at. This affects components that shouldn't // true if the room is being peeked at. This affects components that shouldn't
// logically be shown when peeking, such as a prompt to invite people to a room. // logically be shown when peeking, such as a prompt to invite people to a room.
@ -68,10 +56,6 @@ export default class RoomStatusBar extends React.Component {
// 'you are alone' bar // 'you are alone' bar
onInviteClick: PropTypes.func, onInviteClick: PropTypes.func,
// callback for when the user clicks on the 'stop warning me' button in the
// 'you are alone' bar
onStopWarningClick: PropTypes.func,
// callback for when we do something that changes the size of the // callback for when we do something that changes the size of the
// status bar. This is used to trigger a re-layout in the parent // status bar. This is used to trigger a re-layout in the parent
// component. // component.
@ -122,12 +106,6 @@ export default class RoomStatusBar extends React.Component {
}); });
}; };
_showCallBar() {
return (this.props.callState &&
(this.props.callState !== CallState.Ended && this.props.callState !== CallState.Ringing)
);
}
_onResendAllClick = () => { _onResendAllClick = () => {
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room);
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
@ -159,10 +137,7 @@ export default class RoomStatusBar extends React.Component {
// changed - so we use '0' to indicate normal size, and other values to // changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes. // indicate other sizes.
_getSize() { _getSize() {
if (this._shouldShowConnectionError() || if (this._shouldShowConnectionError()) {
this._showCallBar() ||
this.props.sentMessageAndIsAlone
) {
return STATUS_BAR_EXPANDED; return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0) { } else if (this.state.unsentMessages.length > 0) {
return STATUS_BAR_EXPANDED_LARGE; return STATUS_BAR_EXPANDED_LARGE;
@ -170,22 +145,6 @@ export default class RoomStatusBar extends React.Component {
return STATUS_BAR_HIDDEN; return STATUS_BAR_HIDDEN;
} }
// return suitable content for the image on the left of the status bar.
_getIndicator() {
if (this._showCallBar()) {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
);
}
if (this._shouldShowConnectionError()) {
return null;
}
return null;
}
_shouldShowConnectionError() { _shouldShowConnectionError() {
// no conn bar trumps the "some not sent" msg since you can't resend without // no conn bar trumps the "some not sent" msg since you can't resend without
// a connection! // a connection!
@ -276,25 +235,6 @@ export default class RoomStatusBar extends React.Component {
</div>; </div>;
} }
_getCallStatusText() {
switch (this.props.callState) {
case CallState.CreateOffer:
case CallState.InviteSent:
return _t('Calling...');
case CallState.Connecting:
case CallState.CreateAnswer:
return _t('Call connecting...');
case CallState.Connected:
return _t('Active call');
case CallState.WaitLocalMedia:
if (this.props.callType === CallType.Video) {
return _t('Starting camera...');
} else {
return _t('Starting microphone...');
}
}
}
// return suitable content for the main (text) part of the status bar. // return suitable content for the main (text) part of the status bar.
_getContent() { _getContent() {
if (this._shouldShowConnectionError()) { if (this._shouldShowConnectionError()) {
@ -317,44 +257,14 @@ export default class RoomStatusBar extends React.Component {
return this._getUnsentMessageContent(); return this._getUnsentMessageContent();
} }
if (this._showCallBar()) {
return (
<div className="mx_RoomStatusBar_callBar">
<b>{ this._getCallStatusText() }</b>
</div>
);
}
// If you're alone in the room, and have sent a message, suggest to invite someone
if (this.props.sentMessageAndIsAlone && !this.props.isPeeking) {
return (
<div className="mx_RoomStatusBar_isAlone">
{ _t("There's no one else here! Would you like to <inviteText>invite others</inviteText> " +
"or <nowarnText>stop warning about the empty room</nowarnText>?",
{},
{
'inviteText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="invite" onClick={this.props.onInviteClick}>{ sub }</a>,
'nowarnText': (sub) =>
<a className="mx_RoomStatusBar_resend_link" key="nowarn" onClick={this.props.onStopWarningClick}>{ sub }</a>,
},
) }
</div>
);
}
return null; return null;
} }
render() { render() {
const content = this._getContent(); const content = this._getContent();
const indicator = this._getIndicator();
return ( return (
<div className="mx_RoomStatusBar"> <div className="mx_RoomStatusBar">
<div className="mx_RoomStatusBar_indicator">
{ indicator }
</div>
<div role="alert"> <div role="alert">
{ content } { content }
</div> </div>

View file

@ -41,7 +41,7 @@ import rateLimitedFunc from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils'; import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import eventSearch, {searchPagination} from '../../Searching'; import eventSearch, {searchPagination} from '../../Searching';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
import MainSplit from './MainSplit'; import MainSplit from './MainSplit';
import RightPanel from './RightPanel'; import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils'; import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import {SettingLevel} from "../../settings/SettingLevel"; import {SettingLevel} from "../../settings/SettingLevel";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {IMatrixClientCreds} from "../../MatrixClientPeg"; import {IMatrixClientCreds} from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel"; import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
@ -68,15 +67,16 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
import TintableSvg from "../views/elements/TintableSvg";
import {XOR} from "../../@types/common"; import {XOR} from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
import EffectsOverlay from "../views/elements/effects/EffectsOverlay"; import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
import {containsEmoji} from '../views/elements/effects/effectUtilities'; import {containsEmoji} from '../views/elements/effects/effectUtilities';
import effects from '../views/elements/effects' import effects from '../views/elements/effects'
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import WidgetStore from "../../stores/WidgetStore"; import WidgetStore from "../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../stores/AsyncStore"; import {UPDATE_EVENT} from "../../stores/AsyncStore";
import Notifier from "../../Notifier";
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -132,6 +132,7 @@ export interface IState {
initialEventPixelOffset?: number; initialEventPixelOffset?: number;
// Whether to highlight the event scrolled to // Whether to highlight the event scrolled to
isInitialEventHighlighted?: boolean; isInitialEventHighlighted?: boolean;
replyToEvent?: MatrixEvent;
forwardingEvent?: MatrixEvent; forwardingEvent?: MatrixEvent;
numUnreadMessages: number; numUnreadMessages: number;
draggingFile: boolean; draggingFile: boolean;
@ -150,7 +151,6 @@ export interface IState {
guestsCanJoin: boolean; guestsCanJoin: boolean;
canPeek: boolean; canPeek: boolean;
showApps: boolean; showApps: boolean;
isAlone: boolean;
isPeeking: boolean; isPeeking: boolean;
showingPinned: boolean; showingPinned: boolean;
showReadReceipts: boolean; showReadReceipts: boolean;
@ -223,7 +223,6 @@ export default class RoomView extends React.Component<IProps, IState> {
guestsCanJoin: false, guestsCanJoin: false,
canPeek: false, canPeek: false,
showApps: false, showApps: false,
isAlone: false,
isPeeking: false, isPeeking: false,
showingPinned: false, showingPinned: false,
showReadReceipts: true, showReadReceipts: true,
@ -320,6 +319,7 @@ export default class RoomView extends React.Component<IProps, IState> {
joining: RoomViewStore.isJoining(), joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(), initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(), isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
replyToEvent: RoomViewStore.getQuotingEvent(),
forwardingEvent: RoomViewStore.getForwardingEvent(), forwardingEvent: RoomViewStore.getForwardingEvent(),
// we should only peek once we have a ready client // we should only peek once we have a ready client
shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(), shouldPeek: this.state.matrixClientIsReady && RoomViewStore.shouldPeek(),
@ -511,8 +511,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.props.resizeNotifier.on("middlePanelResized", this.onResize); this.props.resizeNotifier.on("middlePanelResized", this.onResize);
} }
this.onResize(); this.onResize();
document.addEventListener("keydown", this.onNativeKeyDown);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -597,8 +595,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize); this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
} }
document.removeEventListener("keydown", this.onNativeKeyDown);
// Remove RoomStore listener // Remove RoomStore listener
if (this.roomStoreToken) { if (this.roomStoreToken) {
this.roomStoreToken.remove(); this.roomStoreToken.remove();
@ -647,33 +643,6 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
}; };
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
private onNativeKeyDown = ev => {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
switch (ev.key) {
case Key.D:
if (ctrlCmdOnly) {
this.onMuteAudioClick();
handled = true;
}
break;
case Key.E:
if (ctrlCmdOnly) {
this.onMuteVideoClick();
handled = true;
}
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
};
private onReactKeyDown = ev => { private onReactKeyDown = ev => {
let handled = false; let handled = false;
@ -708,9 +677,8 @@ export default class RoomView extends React.Component<IProps, IState> {
private onAction = payload => { private onAction = payload => {
switch (payload.action) { switch (payload.action) {
case 'message_send_failed':
case 'message_sent': case 'message_sent':
this.checkIfAlone(this.state.room); this.checkDesktopNotifications();
break; break;
case 'post_sticker_message': case 'post_sticker_message':
this.injectSticker( this.injectSticker(
@ -1049,33 +1017,17 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
// rate limited because a power level change will emit an event for every member in the room. // rate limited because a power level change will emit an event for every member in the room.
private updateRoomMembers = rateLimitedFunc((dueToMember) => { private updateRoomMembers = rateLimitedFunc(() => {
this.updateDMState(); this.updateDMState();
let memberCountInfluence = 0;
if (dueToMember && dueToMember.membership === "invite" && this.state.room.getInvitedMemberCount() === 0) {
// A member got invited, but the room hasn't detected that change yet. Influence the member
// count by 1 to counteract this.
memberCountInfluence = 1;
}
this.checkIfAlone(this.state.room, memberCountInfluence);
this.updateE2EStatus(this.state.room); this.updateE2EStatus(this.state.room);
}, 500); }, 500);
private checkIfAlone(room: Room, countInfluence?: number) { private checkDesktopNotifications() {
let warnedAboutLonelyRoom = false; const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
if (localStorage) { // if they are not alone prompt the user about notifications so they don't miss replies
warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId)); if (memberCount > 1 && Notifier.shouldShowPrompt()) {
showNotificationsToast(true);
} }
if (warnedAboutLonelyRoom) {
if (this.state.isAlone) this.setState({isAlone: false});
return;
}
let joinedOrInvitedMemberCount = room.getJoinedMemberCount() + room.getInvitedMemberCount();
if (countInfluence) joinedOrInvitedMemberCount += countInfluence;
this.setState({isAlone: joinedOrInvitedMemberCount === 1});
} }
private updateDMState() { private updateDMState() {
@ -1110,14 +1062,6 @@ export default class RoomView extends React.Component<IProps, IState> {
action: 'view_invite', action: 'view_invite',
roomId: this.state.room.roomId, roomId: this.state.room.roomId,
}); });
this.setState({isAlone: false}); // there's a good chance they'll invite someone
};
private onStopAloneWarningClick = () => {
if (localStorage) {
localStorage.setItem('mx_user_alone_warned_' + this.state.room.roomId, String(true));
}
this.setState({isAlone: false});
}; };
private onJoinButtonClicked = () => { private onJoinButtonClicked = () => {
@ -1139,6 +1083,7 @@ export default class RoomView extends React.Component<IProps, IState> {
dis.dispatch({ dis.dispatch({
action: 'join_room', action: 'join_room',
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers }, opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
_type: "unknown", // TODO: instrumentation
}); });
return Promise.resolve(); return Promise.resolve();
}); });
@ -1165,16 +1110,9 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.dataTransfer.dropEffect = 'none'; ev.dataTransfer.dropEffect = 'none';
const items = [...ev.dataTransfer.items]; if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
if (items.length >= 1) { this.setState({ draggingFile: true });
const isDraggingFiles = items.every(function(item) { ev.dataTransfer.dropEffect = 'copy';
return item.kind == 'file';
});
if (isDraggingFiles) {
this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy';
}
} }
}; };
@ -1381,10 +1319,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}; };
private onSettingsClick = () => { private onSettingsClick = () => {
dis.dispatch({ dis.dispatch({ action: "open_room_settings" });
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
});
}; };
private onCancelClick = () => { private onCancelClick = () => {
@ -1815,12 +1750,8 @@ export default class RoomView extends React.Component<IProps, IState> {
isStatusAreaExpanded = this.state.statusBarVisible; isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
sentMessageAndIsAlone={this.state.isAlone}
callState={this.state.callState}
callType={activeCall ? activeCall.type : null}
isPeeking={myMembership !== "join"} isPeeking={myMembership !== "join"}
onInviteClick={this.onInviteButtonClick} onInviteClick={this.onInviteButtonClick}
onStopWarningClick={this.onStopAloneWarningClick}
onVisible={this.onStatusBarVisible} onVisible={this.onStatusBarVisible}
onHidden={this.onStatusBarHidden} onHidden={this.onStatusBarHidden}
/>; />;
@ -1927,6 +1858,7 @@ export default class RoomView extends React.Component<IProps, IState> {
showApps={this.state.showApps} showApps={this.state.showApps}
e2eStatus={this.state.e2eStatus} e2eStatus={this.state.e2eStatus}
resizeNotifier={this.props.resizeNotifier} resizeNotifier={this.props.resizeNotifier}
replyToEvent={this.state.replyToEvent}
permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)} permalinkCreator={this.getPermalinkCreatorForRoom(this.state.room)}
/>; />;
} }
@ -1941,56 +1873,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}; };
} }
if (activeCall) {
let zoomButton; let videoMuteButton;
if (activeCall.type === CallType.Video) {
zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
<TintableSvg
src={require("../../../res/img/element-icons/call/fullscreen.svg")}
width="29"
height="22"
style={{ marginTop: 1, marginRight: 4 }}
/>
</div>
);
videoMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg
src={activeCall.isLocalVideoMuted() ?
require("../../../res/img/element-icons/call/video-muted.svg") :
require("../../../res/img/element-icons/call/video-call.svg")}
alt={activeCall.isLocalVideoMuted() ? _t("Click to unmute video") :
_t("Click to mute video")}
width=""
height="27"
/>
</div>;
}
const voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg
src={activeCall.isMicrophoneMuted() ?
require("../../../res/img/element-icons/call/voice-muted.svg") :
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
alt={activeCall.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21"
height="26"
/>
</div>;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
statusBar =
<div className="mx_RoomView_callStatusBar">
{ voiceMuteButton }
{ videoMuteButton }
{ zoomButton }
{ statusBar }
</div>;
}
// if we have search results, we keep the messagepanel (so that it preserves its // if we have search results, we keep the messagepanel (so that it preserves its
// scroll state), but hide it. // scroll state), but hide it.
let searchResultsPanel; let searchResultsPanel;

View file

@ -704,7 +704,7 @@ export default class ScrollPanel extends React.Component {
if (itemlist.style.height !== newHeight) { if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight; itemlist.style.height = newHeight;
} }
if (sn.scrollTop !== sn.scrollHeight){ if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight; sn.scrollTop = sn.scrollHeight;
} }
debuglog("updateHeight to", newHeight); debuglog("updateHeight to", newHeight);

View file

@ -55,11 +55,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
let toast; let toast;
if (totalCount !== 0) { if (totalCount !== 0) {
const topToast = this.state.toasts[0]; const topToast = this.state.toasts[0];
const {title, icon, key, component, props} = topToast; const {title, icon, key, component, className, props} = topToast;
const toastClasses = classNames("mx_Toast_toast", { const toastClasses = classNames("mx_Toast_toast", {
"mx_Toast_hasIcon": icon, "mx_Toast_hasIcon": icon,
[`mx_Toast_icon_${icon}`]: icon, [`mx_Toast_icon_${icon}`]: icon,
}); }, className);
let countIndicator; let countIndicator;
if (isStacked || this.state.countSeen > 0) { if (isStacked || this.state.countSeen > 0) {

View file

@ -86,7 +86,9 @@ export default class UploadBar extends React.Component {
} }
// MUST use var name 'count' for pluralization to kick in // MUST use var name 'count' for pluralization to kick in
const uploadText = _t("Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)}); const uploadText = _t(
"Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)},
);
return ( return (
<div className="mx_UploadBar"> <div className="mx_UploadBar">

View file

@ -23,7 +23,7 @@ import { _t } from "../../languageHandler";
import { ContextMenuButton } from "./ContextMenu"; import { ContextMenuButton } from "./ContextMenu";
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog"; import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog"; import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal"; import Modal from "../../Modal";
import LogoutDialog from "../views/dialogs/LogoutDialog"; import LogoutDialog from "../views/dialogs/LogoutDialog";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
@ -186,15 +186,22 @@ export default class UserMenu extends React.Component<IProps, IState> {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog); Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
private onSignOutClick = (ev: ButtonEvent) => { private onSignOutClick = async (ev: ButtonEvent) => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog); const cli = MatrixClientPeg.get();
if (!cli || !cli.isCryptoEnabled() || !(await cli.exportRoomKeys())?.length) {
// log out without user prompt if they have no local megolm sessions
dis.dispatch({action: 'logout'});
} else {
Modal.createTrackedDialog('Logout from LeftPanel', '', LogoutDialog);
}
this.setState({contextMenuPosition: null}); // also close the menu this.setState({contextMenuPosition: null}); // also close the menu
}; };
@ -203,6 +210,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
ev.stopPropagation(); ev.stopPropagation();
defaultDispatcher.dispatch({action: 'view_home_page'}); defaultDispatcher.dispatch({action: 'view_home_page'});
this.setState({contextMenuPosition: null}); // also close the menu
}; };
private onCommunitySettingsClick = (ev: ButtonEvent) => { private onCommunitySettingsClick = (ev: ButtonEvent) => {
@ -452,7 +460,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
public render() { public render() {
const avatarSize = 32; // should match border-radius of the avatar const avatarSize = 32; // should match border-radius of the avatar
const displayName = OwnProfileStore.instance.displayName || MatrixClientPeg.get().getUserId(); const userId = MatrixClientPeg.get().getUserId();
const displayName = OwnProfileStore.instance.displayName || userId;
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize); const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName(); const prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
@ -507,7 +516,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
<div className="mx_UserMenu_row"> <div className="mx_UserMenu_row">
<span className="mx_UserMenu_userAvatarContainer"> <span className="mx_UserMenu_userAvatarContainer">
<BaseAvatar <BaseAvatar
idName={displayName} idName={userId}
name={displayName} name={displayName}
url={avatarUrl} url={avatarUrl}
width={avatarSize} width={avatarSize}

View file

@ -26,6 +26,7 @@ import PasswordReset from "../../../PasswordReset";
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils"; import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
import classNames from 'classnames'; import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics";
// Phases // Phases
// Show controls to configure server details // Show controls to configure server details
@ -64,6 +65,12 @@ export default class ForgotPassword extends React.Component {
serverRequiresIdServer: null, serverRequiresIdServer: null,
}; };
constructor(props) {
super(props);
CountlyAnalytics.instance.track("onboarding_forgot_password_begin");
}
componentDidMount() { componentDidMount() {
this.reset = null; this.reset = null;
this._checkServerLiveliness(this.props.serverConfig); this._checkServerLiveliness(this.props.serverConfig);
@ -299,15 +306,19 @@ export default class ForgotPassword extends React.Component {
value={this.state.email} value={this.state.email}
onChange={this.onInputChanged.bind(this, "email")} onChange={this.onInputChanged.bind(this, "email")}
autoFocus autoFocus
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
/> />
</div> </div>
<div className="mx_AuthBody_fieldRow"> <div className="mx_AuthBody_fieldRow">
<Field <Field
name="reset_password" name="reset_password"
type="password" type="password"
label={_t('Password')} label={_t('New Password')}
value={this.state.password} value={this.state.password}
onChange={this.onInputChanged.bind(this, "password")} onChange={this.onInputChanged.bind(this, "password")}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword_blur")}
/> />
<Field <Field
name="reset_password_confirm" name="reset_password_confirm"
@ -315,6 +326,8 @@ export default class ForgotPassword extends React.Component {
label={_t('Confirm')} label={_t('Confirm')}
value={this.state.password2} value={this.state.password2}
onChange={this.onInputChanged.bind(this, "password2")} onChange={this.onInputChanged.bind(this, "password2")}
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_focus")}
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_newPassword2_blur")}
/> />
</div> </div>
<span>{_t( <span>{_t(

View file

@ -1,7 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -16,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, {ComponentProps, ReactNode} from 'react';
import PropTypes from 'prop-types';
import {_t, _td} from '../../../languageHandler'; import {_t, _td} from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Login from '../../../Login'; import Login from '../../../Login';
@ -30,15 +28,13 @@ import SSOButton from "../../views/elements/SSOButton";
import PlatformPeg from '../../../PlatformPeg'; import PlatformPeg from '../../../PlatformPeg';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature"; import {UIFeature} from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics";
// For validating phone numbers without country codes import {IMatrixClientCreds} from "../../../MatrixClientPeg";
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; import ServerConfig from "../../views/auth/ServerConfig";
import PasswordLogin from "../../views/auth/PasswordLogin";
// Phases import SignInToText from "../../views/auth/SignInToText";
// Show controls to configure server details import InlineSpinner from "../../views/elements/InlineSpinner";
const PHASE_SERVER_DETAILS = 0; import Spinner from "../../views/elements/Spinner";
// Show the appropriate login flow(s) for the server
const PHASE_LOGIN = 1;
// Enable phases for login // Enable phases for login
const PHASES_ENABLED = true; const PHASES_ENABLED = true;
@ -54,64 +50,88 @@ _td("Invalid base_url for m.identity_server");
_td("Identity server URL does not appear to be a valid identity server"); _td("Identity server URL does not appear to be a valid identity server");
_td("General failure"); _td("General failure");
interface IProps {
serverConfig: ValidatedServerConfig;
// If true, the component will consider itself busy.
busy?: boolean;
isSyncing?: boolean;
// Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a
// different homeserver without confusing users.
fallbackHsUrl?: string;
defaultDeviceDisplayName?: string;
fragmentAfterLogin?: string;
// Called when the user has logged in. Params:
// - The object returned by the login API
// - The user's password, if applicable, (may be cached in memory for a
// short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn(data: IMatrixClientCreds, password: string): void;
// login shouldn't know or care how registration, password recovery, etc is done.
onRegisterClick(): void;
onForgotPasswordClick?(): void;
onServerConfigChange(config: ValidatedServerConfig): void;
}
enum Phase {
// Show controls to configure server details
ServerDetails,
// Show the appropriate login flow(s) for the server
Login,
}
interface IState {
busy: boolean;
busyLoggingIn?: boolean;
errorText?: ReactNode;
loginIncorrect: boolean;
// can we attempt to log in or are there validation errors?
canTryLogin: boolean;
// used for preserving form values when changing homeserver
username: string;
phoneCountry?: string;
phoneNumber: string;
// Phase of the overall login dialog.
phase: Phase;
// The current login flow, such as password, SSO, etc.
// we need to load the flows from the server
currentFlow?: string;
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: boolean;
serverErrorIsFatal: boolean;
serverDeadError: string;
}
/* /*
* A wire component which glues together login UI components and Login logic * A wire component which glues together login UI components and Login logic
*/ */
export default class LoginComponent extends React.Component { export default class LoginComponent extends React.Component<IProps, IState> {
static propTypes = { private unmounted = false;
// Called when the user has logged in. Params: private loginLogic: Login;
// - The object returned by the login API private readonly stepRendererMap: Record<string, () => ReactNode>;
// - The user's password, if applicable, (may be cached in memory for a
// short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn: PropTypes.func.isRequired,
// If true, the component will consider itself busy.
busy: PropTypes.bool,
// Secondary HS which we try to log into if the user is using
// the default HS but login fails. Useful for migrating to a
// different homeserver without confusing users.
fallbackHsUrl: PropTypes.string,
defaultDeviceDisplayName: PropTypes.string,
// login shouldn't know or care how registration, password recovery,
// etc is done.
onRegisterClick: PropTypes.func.isRequired,
onForgotPasswordClick: PropTypes.func,
onServerConfigChange: PropTypes.func.isRequired,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
isSyncing: PropTypes.bool,
};
constructor(props) { constructor(props) {
super(props); super(props);
this._unmounted = false;
this.state = { this.state = {
busy: false, busy: false,
busyLoggingIn: null, busyLoggingIn: null,
errorText: null, errorText: null,
loginIncorrect: false, loginIncorrect: false,
canTryLogin: true, // can we attempt to log in or are there validation errors? canTryLogin: true,
// used for preserving form values when changing homeserver
username: "", username: "",
phoneCountry: null, phoneCountry: null,
phoneNumber: "", phoneNumber: "",
phase: Phase.Login,
// Phase of the overall login dialog. currentFlow: null,
phase: PHASE_LOGIN,
// The current login flow, such as password, SSO, etc.
currentFlow: null, // we need to load the flows from the server
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: true, serverIsAlive: true,
serverErrorIsFatal: false, serverErrorIsFatal: false,
serverDeadError: "", serverDeadError: "",
@ -119,23 +139,25 @@ export default class LoginComponent extends React.Component {
// map from login step type to a function which will render a control // map from login step type to a function which will render a control
// letting you do that login type // letting you do that login type
this._stepRendererMap = { this.stepRendererMap = {
'm.login.password': this._renderPasswordStep, 'm.login.password': this.renderPasswordStep,
// CAS and SSO are the same thing, modulo the url we link to // CAS and SSO are the same thing, modulo the url we link to
'm.login.cas': () => this._renderSsoStep("cas"), 'm.login.cas': () => this.renderSsoStep("cas"),
'm.login.sso': () => this._renderSsoStep("sso"), 'm.login.sso': () => this.renderSsoStep("sso"),
}; };
CountlyAnalytics.instance.track("onboarding_login_begin");
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
UNSAFE_componentWillMount() { UNSAFE_componentWillMount() {
this._initLoginLogic(); this.initLoginLogic(this.props.serverConfig);
} }
componentWillUnmount() { componentWillUnmount() {
this._unmounted = true; this.unmounted = true;
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@ -145,16 +167,9 @@ export default class LoginComponent extends React.Component {
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
// Ensure that we end up actually logging in to the right place // Ensure that we end up actually logging in to the right place
this._initLoginLogic(newProps.serverConfig.hsUrl, newProps.serverConfig.isUrl); this.initLoginLogic(newProps.serverConfig);
} }
onPasswordLoginError = errorText => {
this.setState({
errorText,
loginIncorrect: Boolean(errorText),
});
};
isBusy = () => this.state.busy || this.props.busy; isBusy = () => this.state.busy || this.props.busy;
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => { onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
@ -191,13 +206,13 @@ export default class LoginComponent extends React.Component {
loginIncorrect: false, loginIncorrect: false,
}); });
this._loginLogic.loginViaPassword( this.loginLogic.loginViaPassword(
username, phoneCountry, phoneNumber, password, username, phoneCountry, phoneNumber, password,
).then((data) => { ).then((data) => {
this.setState({serverIsAlive: true}); // it must be, we logged in. this.setState({serverIsAlive: true}); // it must be, we logged in.
this.props.onLoggedIn(data, password); this.props.onLoggedIn(data, password);
}, (error) => { }, (error) => {
if (this._unmounted) { if (this.unmounted) {
return; return;
} }
let errorText; let errorText;
@ -209,21 +224,23 @@ export default class LoginComponent extends React.Component {
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { } else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError( const errorTop = messageForResourceLimitError(
error.data.limit_type, error.data.limit_type,
error.data.admin_contact, { error.data.admin_contact,
'monthly_active_user': _td( {
"This homeserver has hit its Monthly Active User limit.", 'monthly_active_user': _td(
), "This homeserver has hit its Monthly Active User limit.",
'': _td( ),
"This homeserver has exceeded one of its resource limits.", '': _td(
), "This homeserver has exceeded one of its resource limits.",
}); ),
},
);
const errorDetail = messageForResourceLimitError( const errorDetail = messageForResourceLimitError(
error.data.limit_type, error.data.limit_type,
error.data.admin_contact, { error.data.admin_contact,
'': _td( {
"Please <a>contact your service administrator</a> to continue using this service.", '': _td("Please <a>contact your service administrator</a> to continue using this service."),
), },
}); );
errorText = ( errorText = (
<div> <div>
<div>{errorTop}</div> <div>{errorTop}</div>
@ -250,7 +267,7 @@ export default class LoginComponent extends React.Component {
} }
} else { } else {
// other errors, not specific to doing a password login // other errors, not specific to doing a password login
errorText = this._errorTextFromError(error); errorText = this.errorTextFromError(error);
} }
this.setState({ this.setState({
@ -288,7 +305,7 @@ export default class LoginComponent extends React.Component {
// the busy state. In the case of a full MXID that resolves to the same // the busy state. In the case of a full MXID that resolves to the same
// HS as Element's default HS though, there may not be any server change. // HS as Element's default HS though, there may not be any server change.
// To avoid this trap, we clear busy here. For cases where the server // To avoid this trap, we clear busy here. For cases where the server
// actually has changed, `_initLoginLogic` will be called and manages // actually has changed, `initLoginLogic` will be called and manages
// busy state for its own liveness check. // busy state for its own liveness check.
this.setState({ this.setState({
busy: false, busy: false,
@ -301,7 +318,7 @@ export default class LoginComponent extends React.Component {
message = e.translatedMessage; message = e.translatedMessage;
} }
let errorText = message; let errorText: ReactNode = message;
let discoveryState = {}; let discoveryState = {};
if (AutoDiscoveryUtils.isLivelinessError(e)) { if (AutoDiscoveryUtils.isLivelinessError(e)) {
errorText = this.state.errorText; errorText = this.state.errorText;
@ -327,21 +344,6 @@ export default class LoginComponent extends React.Component {
}); });
}; };
onPhoneNumberBlur = phoneNumber => {
// Validate the phone number entered
if (!PHONE_NUMBER_REGEX.test(phoneNumber)) {
this.setState({
errorText: _t('The phone number entered looks invalid'),
canTryLogin: false,
});
} else {
this.setState({
errorText: null,
canTryLogin: true,
});
}
};
onRegisterClick = ev => { onRegisterClick = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
@ -349,14 +351,14 @@ export default class LoginComponent extends React.Component {
}; };
onTryRegisterClick = ev => { onTryRegisterClick = ev => {
const step = this._getCurrentFlowStep(); const step = this.getCurrentFlowStep();
if (step === 'm.login.sso' || step === 'm.login.cas') { if (step === 'm.login.sso' || step === 'm.login.cas') {
// If we're showing SSO it means that registration is also probably disabled, // If we're showing SSO it means that registration is also probably disabled,
// so intercept the click and instead pretend the user clicked 'Sign in with SSO'. // so intercept the click and instead pretend the user clicked 'Sign in with SSO'.
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas'; const ssoKind = step === 'm.login.sso' ? 'sso' : 'cas';
PlatformPeg.get().startSingleSignOn(this._loginLogic.createTemporaryClient(), ssoKind, PlatformPeg.get().startSingleSignOn(this.loginLogic.createTemporaryClient(), ssoKind,
this.props.fragmentAfterLogin); this.props.fragmentAfterLogin);
} else { } else {
// Don't intercept - just go through to the register page // Don't intercept - just go through to the register page
@ -364,24 +366,21 @@ export default class LoginComponent extends React.Component {
} }
}; };
onServerDetailsNextPhaseClick = () => { private onServerDetailsNextPhaseClick = () => {
this.setState({ this.setState({
phase: PHASE_LOGIN, phase: Phase.Login,
}); });
}; };
onEditServerDetailsClick = ev => { private onEditServerDetailsClick = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.setState({ this.setState({
phase: PHASE_SERVER_DETAILS, phase: Phase.ServerDetails,
}); });
}; };
async _initLoginLogic(hsUrl, isUrl) { private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
isUrl = isUrl || this.props.serverConfig.isUrl;
let isDefaultServer = false; let isDefaultServer = false;
if (this.props.serverConfig.isDefault if (this.props.serverConfig.isDefault
&& hsUrl === this.props.serverConfig.hsUrl && hsUrl === this.props.serverConfig.hsUrl
@ -394,7 +393,7 @@ export default class LoginComponent extends React.Component {
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, { const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName, defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
}); });
this._loginLogic = loginLogic; this.loginLogic = loginLogic;
this.setState({ this.setState({
busy: true, busy: true,
@ -425,7 +424,7 @@ export default class LoginComponent extends React.Component {
if (this.state.serverErrorIsFatal) { if (this.state.serverErrorIsFatal) {
// Server is dead: show server details prompt instead // Server is dead: show server details prompt instead
this.setState({ this.setState({
phase: PHASE_SERVER_DETAILS, phase: Phase.ServerDetails,
}); });
return; return;
} }
@ -434,7 +433,7 @@ export default class LoginComponent extends React.Component {
loginLogic.getFlows().then((flows) => { loginLogic.getFlows().then((flows) => {
// look for a flow where we understand all of the steps. // look for a flow where we understand all of the steps.
for (let i = 0; i < flows.length; i++ ) { for (let i = 0; i < flows.length; i++ ) {
if (!this._isSupportedFlow(flows[i])) { if (!this.isSupportedFlow(flows[i])) {
continue; continue;
} }
@ -443,7 +442,7 @@ export default class LoginComponent extends React.Component {
// that for now). // that for now).
loginLogic.chooseFlow(i); loginLogic.chooseFlow(i);
this.setState({ this.setState({
currentFlow: this._getCurrentFlowStep(), currentFlow: this.getCurrentFlowStep(),
}); });
return; return;
} }
@ -457,7 +456,7 @@ export default class LoginComponent extends React.Component {
}); });
}, (err) => { }, (err) => {
this.setState({ this.setState({
errorText: this._errorTextFromError(err), errorText: this.errorTextFromError(err),
loginIncorrect: false, loginIncorrect: false,
canTryLogin: false, canTryLogin: false,
}); });
@ -468,28 +467,28 @@ export default class LoginComponent extends React.Component {
}); });
} }
_isSupportedFlow(flow) { private isSupportedFlow(flow) {
// technically the flow can have multiple steps, but no one does this // technically the flow can have multiple steps, but no one does this
// for login and loginLogic doesn't support it so we can ignore it. // for login and loginLogic doesn't support it so we can ignore it.
if (!this._stepRendererMap[flow.type]) { if (!this.stepRendererMap[flow.type]) {
console.log("Skipping flow", flow, "due to unsupported login type", flow.type); console.log("Skipping flow", flow, "due to unsupported login type", flow.type);
return false; return false;
} }
return true; return true;
} }
_getCurrentFlowStep() { private getCurrentFlowStep() {
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null; return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
} }
_errorTextFromError(err) { private errorTextFromError(err) {
let errCode = err.errcode; let errCode = err.errcode;
if (!errCode && err.httpStatus) { if (!errCode && err.httpStatus) {
errCode = "HTTP " + err.httpStatus; errCode = "HTTP " + err.httpStatus;
} }
let errorText = _t("Error: Problem communicating with the given homeserver.") + let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") +
(errCode ? " (" + errCode + ")" : ""); (errCode ? " (" + errCode + ")" : "");
if (err.cors === 'rejected') { if (err.cors === 'rejected') {
if (window.location.protocol === 'https:' && if (window.location.protocol === 'https:' &&
@ -499,29 +498,27 @@ export default class LoginComponent extends React.Component {
errorText = <span> errorText = <span>
{ _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " + { _t("Can't connect to homeserver via HTTP when an HTTPS URL is in your browser bar. " +
"Either use HTTPS or <a>enable unsafe scripts</a>.", {}, "Either use HTTPS or <a>enable unsafe scripts</a>.", {},
{ {
'a': (sub) => { 'a': (sub) => {
return <a target="_blank" rel="noreferrer noopener" return <a target="_blank" rel="noreferrer noopener"
href="https://www.google.com/search?&q=enable%20unsafe%20scripts" href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
> >
{ sub } { sub }
</a>; </a>;
},
}, },
) } }) }
</span>; </span>;
} else { } else {
errorText = <span> errorText = <span>
{ _t("Can't connect to homeserver - please check your connectivity, ensure your " + { _t("Can't connect to homeserver - please check your connectivity, ensure your " +
"<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " + "<a>homeserver's SSL certificate</a> is trusted, and that a browser extension " +
"is not blocking requests.", {}, "is not blocking requests.", {},
{ {
'a': (sub) => 'a': (sub) =>
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}> <a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
{ sub } { sub }
</a>, </a>,
}, }) }
) }
</span>; </span>;
} }
} }
@ -529,18 +526,16 @@ export default class LoginComponent extends React.Component {
return errorText; return errorText;
} }
renderServerComponent() { private renderServerComponent() {
const ServerConfig = sdk.getComponent("auth.ServerConfig");
if (SdkConfig.get()['disable_custom_urls']) { if (SdkConfig.get()['disable_custom_urls']) {
return null; return null;
} }
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) { if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
return null; return null;
} }
const serverDetailsProps = {}; const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
if (PHASES_ENABLED) { if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next"); serverDetailsProps.submitText = _t("Next");
@ -555,8 +550,8 @@ export default class LoginComponent extends React.Component {
/>; />;
} }
renderLoginComponentForStep() { private renderLoginComponentForStep() {
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) { if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
return null; return null;
} }
@ -566,7 +561,7 @@ export default class LoginComponent extends React.Component {
return null; return null;
} }
const stepRenderer = this._stepRendererMap[step]; const stepRenderer = this.stepRendererMap[step];
if (stepRenderer) { if (stepRenderer) {
return stepRenderer(); return stepRenderer();
@ -575,9 +570,7 @@ export default class LoginComponent extends React.Component {
return null; return null;
} }
_renderPasswordStep = () => { private renderPasswordStep = () => {
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
let onEditServerDetailsClick = null; let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link. // If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
@ -586,29 +579,25 @@ export default class LoginComponent extends React.Component {
return ( return (
<PasswordLogin <PasswordLogin
onSubmit={this.onPasswordLogin} onSubmit={this.onPasswordLogin}
onError={this.onPasswordLoginError} onEditServerDetailsClick={onEditServerDetailsClick}
onEditServerDetailsClick={onEditServerDetailsClick} username={this.state.username}
initialUsername={this.state.username} phoneCountry={this.state.phoneCountry}
initialPhoneCountry={this.state.phoneCountry} phoneNumber={this.state.phoneNumber}
initialPhoneNumber={this.state.phoneNumber} onUsernameChanged={this.onUsernameChanged}
onUsernameChanged={this.onUsernameChanged} onUsernameBlur={this.onUsernameBlur}
onUsernameBlur={this.onUsernameBlur} onPhoneCountryChanged={this.onPhoneCountryChanged}
onPhoneCountryChanged={this.onPhoneCountryChanged} onPhoneNumberChanged={this.onPhoneNumberChanged}
onPhoneNumberChanged={this.onPhoneNumberChanged} onForgotPasswordClick={this.props.onForgotPasswordClick}
onPhoneNumberBlur={this.onPhoneNumberBlur} loginIncorrect={this.state.loginIncorrect}
onForgotPasswordClick={this.props.onForgotPasswordClick} serverConfig={this.props.serverConfig}
loginIncorrect={this.state.loginIncorrect} disableSubmit={this.isBusy()}
serverConfig={this.props.serverConfig} busy={this.props.isSyncing || this.state.busyLoggingIn}
disableSubmit={this.isBusy()}
busy={this.props.isSyncing || this.state.busyLoggingIn}
/> />
); );
}; };
_renderSsoStep = loginType => { private renderSsoStep = loginType => {
const SignInToText = sdk.getComponent('views.auth.SignInToText');
let onEditServerDetailsClick = null; let onEditServerDetailsClick = null;
// If custom URLs are allowed, wire up the server details edit link. // If custom URLs are allowed, wire up the server details edit link.
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) { if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
@ -629,7 +618,7 @@ export default class LoginComponent extends React.Component {
<SSOButton <SSOButton
className="mx_Login_sso_link mx_Login_submit" className="mx_Login_sso_link mx_Login_submit"
matrixClient={this._loginLogic.createTemporaryClient()} matrixClient={this.loginLogic.createTemporaryClient()}
loginType={loginType} loginType={loginType}
fragmentAfterLogin={this.props.fragmentAfterLogin} fragmentAfterLogin={this.props.fragmentAfterLogin}
/> />
@ -638,12 +627,10 @@ export default class LoginComponent extends React.Component {
}; };
render() { render() {
const Loader = sdk.getComponent("elements.Spinner");
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
const AuthHeader = sdk.getComponent("auth.AuthHeader"); const AuthHeader = sdk.getComponent("auth.AuthHeader");
const AuthBody = sdk.getComponent("auth.AuthBody"); const AuthBody = sdk.getComponent("auth.AuthBody");
const loader = this.isBusy() && !this.state.busyLoggingIn ? const loader = this.isBusy() && !this.state.busyLoggingIn ?
<div className="mx_Login_loader"><Loader /></div> : null; <div className="mx_Login_loader"><Spinner /></div> : null;
const errorText = this.state.errorText; const errorText = this.state.errorText;

View file

@ -1,77 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler';
import AuthPage from "../../views/auth/AuthPage";
export default class PostRegistration extends React.Component {
static propTypes = {
onComplete: PropTypes.func.isRequired,
};
state = {
avatarUrl: null,
errorString: null,
busy: false,
};
componentDidMount() {
// There is some assymetry between ChangeDisplayName and ChangeAvatar,
// as ChangeDisplayName will auto-get the name but ChangeAvatar expects
// the URL to be passed to you (because it's also used for room avatars).
const cli = MatrixClientPeg.get();
this.setState({busy: true});
const self = this;
cli.getProfileInfo(cli.credentials.userId).then(function(result) {
self.setState({
avatarUrl: MatrixClientPeg.get().mxcUrlToHttp(result.avatar_url),
busy: false,
});
}, function(error) {
self.setState({
errorString: _t("Failed to fetch avatar URL"),
busy: false,
});
});
}
render() {
const ChangeDisplayName = sdk.getComponent('settings.ChangeDisplayName');
const ChangeAvatar = sdk.getComponent('settings.ChangeAvatar');
const AuthHeader = sdk.getComponent('auth.AuthHeader');
const AuthBody = sdk.getComponent("auth.AuthBody");
return (
<AuthPage>
<AuthHeader />
<AuthBody>
<div className="mx_Login_profile">
{ _t('Set a display name:') }
<ChangeDisplayName />
{ _t('Upload an avatar:') }
<ChangeAvatar
initialAvatarUrl={this.state.avatarUrl} />
<button onClick={this.props.onComplete}>{ _t('Continue') }</button>
{ this.state.errorString }
</div>
</AuthBody>
</AuthPage>
);
}
}

View file

@ -1,8 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2017, 2018, 2019, 2020 The Matrix.org Foundation C.I.C.
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -18,8 +15,9 @@ limitations under the License.
*/ */
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import React from 'react'; import React, {ComponentProps, ReactNode} from 'react';
import PropTypes from 'prop-types'; import {MatrixClient} from "matrix-js-sdk/src/client";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t, _td } from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
@ -34,36 +32,96 @@ import Login from "../../../Login";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
// Phases // Phases
// Show controls to configure server details enum Phase {
const PHASE_SERVER_DETAILS = 0; // Show controls to configure server details
// Show the appropriate registration flow(s) for the server ServerDetails = 0,
const PHASE_REGISTRATION = 1; // Show the appropriate registration flow(s) for the server
Registration = 1,
}
interface IProps {
serverConfig: ValidatedServerConfig;
defaultDeviceDisplayName: string;
email?: string;
brand?: string;
clientSecret?: string;
sessionId?: string;
idSid?: string;
// Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
// - The user's password, if available and applicable (may be cached in memory
// for a short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn(params: {
userId: string;
deviceId: string
homeserverUrl: string;
identityServerUrl?: string;
accessToken: string;
}, password: string): void;
makeRegistrationUrl(params: {
/* eslint-disable camelcase */
client_secret: string;
hs_url: string;
is_url?: string;
session_id: string;
/* eslint-enable camelcase */
}): void;
// registration shouldn't know or care how login is done.
onLoginClick(): void;
onServerConfigChange(config: ValidatedServerConfig): void;
}
interface IState {
busy: boolean;
errorText?: ReactNode;
// true if we're waiting for the user to complete
// We remember the values entered by the user because
// the registration form will be unmounted during the
// course of registration, but if there's an error we
// want to bring back the registration form with the
// values the user entered still in it. We can keep
// them in this component's state since this component
// persist for the duration of the registration process.
formVals: Record<string, string>;
// user-interactive auth
// If we've been given a session ID, we're resuming
// straight back into UI auth
doingUIAuth: boolean;
// If set, we've registered but are not going to log
// the user in to their new account automatically.
completedNoSignin: boolean;
serverType: ServerType.FREE | ServerType.PREMIUM | ServerType.ADVANCED;
// Phase of the overall registration dialog.
phase: Phase;
flows: {
stages: string[];
}[];
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: boolean;
serverErrorIsFatal: boolean;
serverDeadError: string;
// Our matrix client - part of state because we can't render the UI auth
// component without it.
matrixClient?: MatrixClient;
// whether the HS requires an ID server to register with a threepid
serverRequiresIdServer?: boolean;
// The user ID we've just registered
registeredUsername?: string;
// if a different user ID to the one we just registered is logged in,
// this is the user ID that's logged in.
differentLoggedInUserId?: string;
}
// Enable phases for registration // Enable phases for registration
const PHASES_ENABLED = true; const PHASES_ENABLED = true;
export default class Registration extends React.Component { export default class Registration extends React.Component<IProps, IState> {
static propTypes = {
// Called when the user has logged in. Params:
// - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken
// - The user's password, if available and applicable (may be cached in memory
// for a short time so the user is not required to re-enter their password
// for operations like uploading cross-signing keys).
onLoggedIn: PropTypes.func.isRequired,
clientSecret: PropTypes.string,
sessionId: PropTypes.string,
makeRegistrationUrl: PropTypes.func.isRequired,
idSid: PropTypes.string,
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,
brand: PropTypes.string,
email: PropTypes.string,
// registration shouldn't know or care how login is done.
onLoginClick: PropTypes.func.isRequired,
onServerConfigChange: PropTypes.func.isRequired,
defaultDeviceDisplayName: PropTypes.string,
};
constructor(props) { constructor(props) {
super(props); super(props);
@ -71,56 +129,22 @@ export default class Registration extends React.Component {
this.state = { this.state = {
busy: false, busy: false,
errorText: null, errorText: null,
// We remember the values entered by the user because
// the registration form will be unmounted during the
// course of registration, but if there's an error we
// want to bring back the registration form with the
// values the user entered still in it. We can keep
// them in this component's state since this component
// persist for the duration of the registration process.
formVals: { formVals: {
email: this.props.email, email: this.props.email,
}, },
// true if we're waiting for the user to complete
// user-interactive auth
// If we've been given a session ID, we're resuming
// straight back into UI auth
doingUIAuth: Boolean(this.props.sessionId), doingUIAuth: Boolean(this.props.sessionId),
serverType, serverType,
// Phase of the overall registration dialog. phase: Phase.Registration,
phase: PHASE_REGISTRATION,
flows: null, flows: null,
// If set, we've registered but are not going to log
// the user in to their new account automatically.
completedNoSignin: false, completedNoSignin: false,
// We perform liveliness checks later, but for now suppress the errors.
// We also track the server dead errors independently of the regular errors so
// that we can render it differently, and override any other error the user may
// be seeing.
serverIsAlive: true, serverIsAlive: true,
serverErrorIsFatal: false, serverErrorIsFatal: false,
serverDeadError: "", serverDeadError: "",
// Our matrix client - part of state because we can't render the UI auth
// component without it.
matrixClient: null,
// whether the HS requires an ID server to register with a threepid
serverRequiresIdServer: null,
// The user ID we've just registered
registeredUsername: null,
// if a different user ID to the one we just registered is logged in,
// this is the user ID that's logged in.
differentLoggedInUserId: null,
}; };
} }
componentDidMount() { componentDidMount() {
this._unmounted = false; this.replaceClient(this.props.serverConfig);
this._replaceClient();
} }
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event // TODO: [REACT-WARNING] Replace with appropriate lifecycle event
@ -129,7 +153,7 @@ export default class Registration extends React.Component {
if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl && if (newProps.serverConfig.hsUrl === this.props.serverConfig.hsUrl &&
newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return; newProps.serverConfig.isUrl === this.props.serverConfig.isUrl) return;
this._replaceClient(newProps.serverConfig); this.replaceClient(newProps.serverConfig);
// Handle cases where the user enters "https://matrix.org" for their server // Handle cases where the user enters "https://matrix.org" for their server
// from the advanced option - we should default to FREE at that point. // from the advanced option - we should default to FREE at that point.
@ -138,25 +162,25 @@ export default class Registration extends React.Component {
// Reset the phase to default phase for the server type. // Reset the phase to default phase for the server type.
this.setState({ this.setState({
serverType, serverType,
phase: this.getDefaultPhaseForServerType(serverType), phase: Registration.getDefaultPhaseForServerType(serverType),
}); });
} }
} }
getDefaultPhaseForServerType(type) { private static getDefaultPhaseForServerType(type: IState["serverType"]) {
switch (type) { switch (type) {
case ServerType.FREE: { case ServerType.FREE: {
// Move directly to the registration phase since the server // Move directly to the registration phase since the server
// details are fixed. // details are fixed.
return PHASE_REGISTRATION; return Phase.Registration;
} }
case ServerType.PREMIUM: case ServerType.PREMIUM:
case ServerType.ADVANCED: case ServerType.ADVANCED:
return PHASE_SERVER_DETAILS; return Phase.ServerDetails;
} }
} }
onServerTypeChange = type => { private onServerTypeChange = (type: IState["serverType"]) => {
this.setState({ this.setState({
serverType: type, serverType: type,
}); });
@ -181,11 +205,11 @@ export default class Registration extends React.Component {
// Reset the phase to default phase for the server type. // Reset the phase to default phase for the server type.
this.setState({ this.setState({
phase: this.getDefaultPhaseForServerType(type), phase: Registration.getDefaultPhaseForServerType(type),
}); });
}; };
async _replaceClient(serverConfig) { private async replaceClient(serverConfig: ValidatedServerConfig) {
this.setState({ this.setState({
errorText: null, errorText: null,
serverDeadError: null, serverDeadError: null,
@ -194,7 +218,6 @@ export default class Registration extends React.Component {
// the UI auth component while we don't have a matrix client) // the UI auth component while we don't have a matrix client)
busy: true, busy: true,
}); });
if (!serverConfig) serverConfig = this.props.serverConfig;
// Do a liveliness check on the URLs // Do a liveliness check on the URLs
try { try {
@ -246,7 +269,7 @@ export default class Registration extends React.Component {
// do SSO instead. If we've already started the UI Auth process though, we don't // do SSO instead. If we've already started the UI Auth process though, we don't
// need to. // need to.
if (!this.state.doingUIAuth) { if (!this.state.doingUIAuth) {
await this._makeRegisterRequest(null); await this.makeRegisterRequest(null);
// This should never succeed since we specified no auth object. // This should never succeed since we specified no auth object.
console.log("Expecting 401 from register request but got success!"); console.log("Expecting 401 from register request but got success!");
} }
@ -287,7 +310,7 @@ export default class Registration extends React.Component {
} }
} }
onFormSubmit = formVals => { private onFormSubmit = formVals => {
this.setState({ this.setState({
errorText: "", errorText: "",
busy: true, busy: true,
@ -296,7 +319,7 @@ export default class Registration extends React.Component {
}); });
}; };
_requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => { private requestEmailToken = (emailAddress, clientSecret, sendAttempt, sessionId) => {
return this.state.matrixClient.requestRegisterEmailToken( return this.state.matrixClient.requestRegisterEmailToken(
emailAddress, emailAddress,
clientSecret, clientSecret,
@ -310,28 +333,26 @@ export default class Registration extends React.Component {
); );
} }
_onUIAuthFinished = async (success, response, extra) => { private onUIAuthFinished = async (success, response, extra) => {
if (!success) { if (!success) {
let msg = response.message || response.toString(); let msg = response.message || response.toString();
// can we give a better error message? // can we give a better error message?
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') { if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
const errorTop = messageForResourceLimitError( const errorTop = messageForResourceLimitError(
response.data.limit_type, response.data.limit_type,
response.data.admin_contact, { response.data.admin_contact,
'monthly_active_user': _td( {
"This homeserver has hit its Monthly Active User limit.", 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
), '': _td("This homeserver has exceeded one of its resource limits."),
'': _td( },
"This homeserver has exceeded one of its resource limits.", );
),
});
const errorDetail = messageForResourceLimitError( const errorDetail = messageForResourceLimitError(
response.data.limit_type, response.data.limit_type,
response.data.admin_contact, { response.data.admin_contact,
'': _td( {
"Please <a>contact your service administrator</a> to continue using this service.", '': _td("Please <a>contact your service administrator</a> to continue using this service."),
), },
}); );
msg = <div> msg = <div>
<p>{errorTop}</p> <p>{errorTop}</p>
<p>{errorDetail}</p> <p>{errorDetail}</p>
@ -339,7 +360,7 @@ export default class Registration extends React.Component {
} else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) { } else if (response.required_stages && response.required_stages.indexOf('m.login.msisdn') > -1) {
let msisdnAvailable = false; let msisdnAvailable = false;
for (const flow of response.available_flows) { for (const flow of response.available_flows) {
msisdnAvailable |= flow.stages.indexOf('m.login.msisdn') > -1; msisdnAvailable = msisdnAvailable || flow.stages.includes('m.login.msisdn');
} }
if (!msisdnAvailable) { if (!msisdnAvailable) {
msg = _t('This server does not support authentication with a phone number.'); msg = _t('This server does not support authentication with a phone number.');
@ -358,6 +379,10 @@ export default class Registration extends React.Component {
const newState = { const newState = {
doingUIAuth: false, doingUIAuth: false,
registeredUsername: response.user_id, registeredUsername: response.user_id,
differentLoggedInUserId: null,
completedNoSignin: false,
// we're still busy until we get unmounted: don't show the registration form again
busy: true,
}; };
// The user came in through an email validation link. To avoid overwriting // The user came in through an email validation link. To avoid overwriting
@ -372,8 +397,6 @@ export default class Registration extends React.Component {
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`, `Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
); );
newState.differentLoggedInUserId = sessionOwner; newState.differentLoggedInUserId = sessionOwner;
} else {
newState.differentLoggedInUserId = null;
} }
if (response.access_token) { if (response.access_token) {
@ -385,9 +408,7 @@ export default class Registration extends React.Component {
accessToken: response.access_token, accessToken: response.access_token,
}, this.state.formVals.password); }, this.state.formVals.password);
this._setupPushers(); this.setupPushers();
// we're still busy until we get unmounted: don't show the registration form again
newState.busy = true;
} else { } else {
newState.busy = false; newState.busy = false;
newState.completedNoSignin = true; newState.completedNoSignin = true;
@ -396,7 +417,7 @@ export default class Registration extends React.Component {
this.setState(newState); this.setState(newState);
}; };
_setupPushers() { private setupPushers() {
if (!this.props.brand) { if (!this.props.brand) {
return Promise.resolve(); return Promise.resolve();
} }
@ -419,38 +440,38 @@ export default class Registration extends React.Component {
}); });
} }
onLoginClick = ev => { private onLoginClick = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.props.onLoginClick(); this.props.onLoginClick();
}; };
onGoToFormClicked = ev => { private onGoToFormClicked = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this._replaceClient(); this.replaceClient(this.props.serverConfig);
this.setState({ this.setState({
busy: false, busy: false,
doingUIAuth: false, doingUIAuth: false,
phase: PHASE_REGISTRATION, phase: Phase.Registration,
}); });
}; };
onServerDetailsNextPhaseClick = async () => { private onServerDetailsNextPhaseClick = async () => {
this.setState({ this.setState({
phase: PHASE_REGISTRATION, phase: Phase.Registration,
}); });
}; };
onEditServerDetailsClick = ev => { private onEditServerDetailsClick = ev => {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this.setState({ this.setState({
phase: PHASE_SERVER_DETAILS, phase: Phase.ServerDetails,
}); });
}; };
_makeRegisterRequest = auth => { private makeRegisterRequest = auth => {
// We inhibit login if we're trying to register with an email address: this // We inhibit login if we're trying to register with an email address: this
// avoids a lot of complex race conditions that can occur if we try to log // avoids a lot of complex race conditions that can occur if we try to log
// the user in one one or both of the tabs they might end up with after // the user in one one or both of the tabs they might end up with after
@ -466,13 +487,15 @@ export default class Registration extends React.Component {
username: this.state.formVals.username, username: this.state.formVals.username,
password: this.state.formVals.password, password: this.state.formVals.password,
initial_device_display_name: this.props.defaultDeviceDisplayName, initial_device_display_name: this.props.defaultDeviceDisplayName,
auth: undefined,
inhibit_login: undefined,
}; };
if (auth) registerParams.auth = auth; if (auth) registerParams.auth = auth;
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin; if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
return this.state.matrixClient.registerRequest(registerParams); return this.state.matrixClient.registerRequest(registerParams);
}; };
_getUIAuthInputs() { private getUIAuthInputs() {
return { return {
emailAddress: this.state.formVals.email, emailAddress: this.state.formVals.email,
phoneCountry: this.state.formVals.phoneCountry, phoneCountry: this.state.formVals.phoneCountry,
@ -483,7 +506,7 @@ export default class Registration extends React.Component {
// Links to the login page shown after registration is completed are routed through this // Links to the login page shown after registration is completed are routed through this
// which checks the user hasn't already logged in somewhere else (perhaps we should do // which checks the user hasn't already logged in somewhere else (perhaps we should do
// this more generally?) // this more generally?)
_onLoginClickWithCheck = async ev => { private onLoginClickWithCheck = async ev => {
ev.preventDefault(); ev.preventDefault();
const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true}); const sessionLoaded = await Lifecycle.loadSession({ignoreGuest: true});
@ -493,7 +516,7 @@ export default class Registration extends React.Component {
} }
}; };
renderServerComponent() { private renderServerComponent() {
const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector"); const ServerTypeSelector = sdk.getComponent("auth.ServerTypeSelector");
const ServerConfig = sdk.getComponent("auth.ServerConfig"); const ServerConfig = sdk.getComponent("auth.ServerConfig");
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig"); const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
@ -502,11 +525,16 @@ export default class Registration extends React.Component {
return null; return null;
} }
// Hide the server picker once the user is doing UI Auth unless encountered a fatal server error
if (this.state.phase !== Phase.ServerDetails && this.state.doingUIAuth && !this.state.serverErrorIsFatal) {
return null;
}
// If we're on a different phase, we only show the server type selector, // If we're on a different phase, we only show the server type selector,
// which is always shown if we allow custom URLs at all. // which is always shown if we allow custom URLs at all.
// (if there's a fatal server error, we need to show the full server // (if there's a fatal server error, we need to show the full server
// config as the user may need to change servers to resolve the error). // config as the user may need to change servers to resolve the error).
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS && !this.state.serverErrorIsFatal) { if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails && !this.state.serverErrorIsFatal) {
return <div> return <div>
<ServerTypeSelector <ServerTypeSelector
selected={this.state.serverType} selected={this.state.serverType}
@ -515,7 +543,7 @@ export default class Registration extends React.Component {
</div>; </div>;
} }
const serverDetailsProps = {}; const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
if (PHASES_ENABLED) { if (PHASES_ENABLED) {
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick; serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
serverDetailsProps.submitText = _t("Next"); serverDetailsProps.submitText = _t("Next");
@ -554,8 +582,8 @@ export default class Registration extends React.Component {
</div>; </div>;
} }
renderRegisterComponent() { private renderRegisterComponent() {
if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) { if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
return null; return null;
} }
@ -566,10 +594,10 @@ export default class Registration extends React.Component {
if (this.state.matrixClient && this.state.doingUIAuth) { if (this.state.matrixClient && this.state.doingUIAuth) {
return <InteractiveAuth return <InteractiveAuth
matrixClient={this.state.matrixClient} matrixClient={this.state.matrixClient}
makeRequest={this._makeRegisterRequest} makeRequest={this.makeRegisterRequest}
onAuthFinished={this._onUIAuthFinished} onAuthFinished={this.onUIAuthFinished}
inputs={this._getUIAuthInputs()} inputs={this.getUIAuthInputs()}
requestEmailToken={this._requestEmailToken} requestEmailToken={this.requestEmailToken}
sessionId={this.props.sessionId} sessionId={this.props.sessionId}
clientSecret={this.props.clientSecret} clientSecret={this.props.clientSecret}
emailSid={this.props.idSid} emailSid={this.props.idSid}
@ -582,17 +610,6 @@ export default class Registration extends React.Component {
<Spinner /> <Spinner />
</div>; </div>;
} else if (this.state.flows.length) { } else if (this.state.flows.length) {
let onEditServerDetailsClick = null;
// If custom URLs are allowed and we haven't selected the Free server type, wire
// up the server details edit link.
if (
PHASES_ENABLED &&
!SdkConfig.get()['disable_custom_urls'] &&
this.state.serverType !== ServerType.FREE
) {
onEditServerDetailsClick = this.onEditServerDetailsClick;
}
return <RegistrationForm return <RegistrationForm
defaultUsername={this.state.formVals.username} defaultUsername={this.state.formVals.username}
defaultEmail={this.state.formVals.email} defaultEmail={this.state.formVals.email}
@ -600,7 +617,6 @@ export default class Registration extends React.Component {
defaultPhoneNumber={this.state.formVals.phoneNumber} defaultPhoneNumber={this.state.formVals.phoneNumber}
defaultPassword={this.state.formVals.password} defaultPassword={this.state.formVals.password}
onRegisterClick={this.onFormSubmit} onRegisterClick={this.onFormSubmit}
onEditServerDetailsClick={onEditServerDetailsClick}
flows={this.state.flows} flows={this.state.flows}
serverConfig={this.props.serverConfig} serverConfig={this.props.serverConfig}
canSubmit={!this.state.serverErrorIsFatal} canSubmit={!this.state.serverErrorIsFatal}
@ -640,7 +656,7 @@ export default class Registration extends React.Component {
// Only show the 'go back' button if you're not looking at the form // Only show the 'go back' button if you're not looking at the form
let goBack; let goBack;
if ((PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) || this.state.doingUIAuth) { if ((PHASES_ENABLED && this.state.phase !== Phase.Registration) || this.state.doingUIAuth) {
goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#"> goBack = <a className="mx_AuthBody_changeFlow" onClick={this.onGoToFormClicked} href="#">
{ _t('Go back') } { _t('Go back') }
</a>; </a>;
@ -658,7 +674,7 @@ export default class Registration extends React.Component {
loggedInUserId: this.state.differentLoggedInUserId, loggedInUserId: this.state.differentLoggedInUserId,
}, },
)}</p> )}</p>
<p><AccessibleButton element="span" className="mx_linkButton" onClick={this._onLoginClickWithCheck}> <p><AccessibleButton element="span" className="mx_linkButton" onClick={this.onLoginClickWithCheck}>
{_t("Continue with previous account")} {_t("Continue with previous account")}
</AccessibleButton></p> </AccessibleButton></p>
</div>; </div>;
@ -667,7 +683,7 @@ export default class Registration extends React.Component {
regDoneText = <h3>{_t( regDoneText = <h3>{_t(
"<a>Log in</a> to your new account.", {}, "<a>Log in</a> to your new account.", {},
{ {
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>, a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
}, },
)}</h3>; )}</h3>;
} else { } else {
@ -677,7 +693,7 @@ export default class Registration extends React.Component {
regDoneText = <h3>{_t( regDoneText = <h3>{_t(
"You can now close this window or <a>log in</a> to your new account.", {}, "You can now close this window or <a>log in</a> to your new account.", {},
{ {
a: (sub) => <a href="#/login" onClick={this._onLoginClickWithCheck}>{sub}</a>, a: (sub) => <a href="#/login" onClick={this.onLoginClickWithCheck}>{sub}</a>,
}, },
)}</h3>; )}</h3>;
} }
@ -686,11 +702,48 @@ export default class Registration extends React.Component {
{ regDoneText } { regDoneText }
</div>; </div>;
} else { } else {
let yourMatrixAccountText: ReactNode = _t('Create your Matrix account on %(serverName)s', {
serverName: this.props.serverConfig.hsName,
});
if (this.props.serverConfig.hsNameIsDifferent) {
const TextWithTooltip = sdk.getComponent("elements.TextWithTooltip");
yourMatrixAccountText = _t('Create your Matrix account on <underlinedServerName />', {}, {
'underlinedServerName': () => {
return <TextWithTooltip
class="mx_Login_underlinedServerName"
tooltip={this.props.serverConfig.hsUrl}
>
{this.props.serverConfig.hsName}
</TextWithTooltip>;
},
});
}
// If custom URLs are allowed, user is not doing UIA flows and they haven't selected the Free server type,
// wire up the server details edit link.
let editLink = null;
if (PHASES_ENABLED &&
!SdkConfig.get()['disable_custom_urls'] &&
this.state.serverType !== ServerType.FREE &&
!this.state.doingUIAuth
) {
editLink = (
<a className="mx_AuthBody_editServerDetails" href="#" onClick={this.onEditServerDetailsClick}>
{_t('Change')}
</a>
);
}
body = <div> body = <div>
<h2>{ _t('Create your account') }</h2> <h2>{ _t('Create your account') }</h2>
{ errorText } { errorText }
{ serverDeadSection } { serverDeadSection }
{ this.renderServerComponent() } { this.renderServerComponent() }
{ this.state.phase !== Phase.ServerDetails && <h3>
{yourMatrixAccountText}
{editLink}
</h3> }
{ this.renderRegisterComponent() } { this.renderRegisterComponent() }
{ goBack } { goBack }
{ signIn } { signIn }

View file

@ -17,6 +17,7 @@ limitations under the License.
import React, {createRef} from 'react'; import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics";
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
@ -45,6 +46,8 @@ export default class CaptchaForm extends React.Component {
this._captchaWidgetId = null; this._captchaWidgetId = null;
this._recaptchaContainer = createRef(); this._recaptchaContainer = createRef();
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
} }
componentDidMount() { componentDidMount() {
@ -99,10 +102,16 @@ export default class CaptchaForm extends React.Component {
console.log("Loaded recaptcha script."); console.log("Loaded recaptcha script.");
try { try {
this._renderRecaptcha(DIV_ID); this._renderRecaptcha(DIV_ID);
// clear error if re-rendered
this.setState({
errorText: null,
});
CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded");
} catch (e) { } catch (e) {
this.setState({ this.setState({
errorText: e.toString(), errorText: e.toString(),
}); });
CountlyAnalytics.instance.track("onboarding_grecaptcha_error", { error: e.toString() });
} }
} }

View file

@ -123,7 +123,7 @@ export default class CountryDropdown extends React.Component {
const options = displayedCountries.map((country) => { const options = displayedCountries.map((country) => {
return <div className="mx_CountryDropdown_option" key={country.iso2}> return <div className="mx_CountryDropdown_option" key={country.iso2}>
{ this._flagImgForIso2(country.iso2) } { this._flagImgForIso2(country.iso2) }
{ country.name } (+{ country.prefix }) { _t(country.name) } (+{ country.prefix })
</div>; </div>;
}); });

View file

@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics";
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -189,6 +190,7 @@ export class RecaptchaAuthEntry extends React.Component {
} }
_onCaptchaResponse = response => { _onCaptchaResponse = response => {
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
this.props.submitAuthDict({ this.props.submitAuthDict({
type: RecaptchaAuthEntry.LOGIN_TYPE, type: RecaptchaAuthEntry.LOGIN_TYPE,
response: response, response: response,
@ -297,6 +299,8 @@ export class TermsAuthEntry extends React.Component {
toggledPolicies: initToggles, toggledPolicies: initToggles,
policies: pickedPolicies, policies: pickedPolicies,
}; };
CountlyAnalytics.instance.track("onboarding_terms_begin");
} }
@ -326,8 +330,12 @@ export class TermsAuthEntry extends React.Component {
allChecked = allChecked && checked; allChecked = allChecked && checked;
} }
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE}); if (allChecked) {
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")}); this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
CountlyAnalytics.instance.track("onboarding_terms_complete");
} else {
this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
}
}; };
render() { render() {
@ -413,12 +421,12 @@ export class EmailIdentityAuthEntry extends React.Component {
return <Spinner />; return <Spinner />;
} else { } else {
return ( return (
<div> <div className="mx_InteractiveAuthEntryComponents_emailWrapper">
<p>{ _t("An email has been sent to %(emailAddress)s", <p>{ _t("A confirmation email has been sent to %(emailAddress)s",
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> }, { emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
) } ) }
</p> </p>
<p>{ _t("Please check your email to continue registration.") }</p> <p>{ _t("Open the link in the email to continue registration.") }</p>
</div> </div>
); );
} }

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