Merge branch 'develop' into feature_confetti#14676
|
@ -1,36 +1,15 @@
|
|||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||
|
||||
src/ImageUtils.js
|
||||
src/Markdown.js
|
||||
src/Rooms.js
|
||||
src/Unread.js
|
||||
src/Velociraptor.js
|
||||
src/components/structures/RoomDirectory.js
|
||||
src/components/structures/ScrollPanel.js
|
||||
src/components/structures/UploadBar.js
|
||||
src/components/views/elements/AddressSelector.js
|
||||
src/components/views/elements/DirectorySearchBox.js
|
||||
src/components/views/messages/MFileBody.js
|
||||
src/components/views/messages/TextualBody.js
|
||||
src/components/views/rooms/AuxPanel.js
|
||||
src/components/views/rooms/LinkPreviewWidget.js
|
||||
src/components/views/rooms/MemberList.js
|
||||
src/components/views/rooms/RoomPreviewBar.js
|
||||
src/components/views/settings/ChangeAvatar.js
|
||||
src/components/views/settings/DevicesPanel.js
|
||||
src/components/views/settings/Notifications.js
|
||||
src/rageshake/rageshake.js
|
||||
src/ratelimitedfunc.js
|
||||
src/utils/DMRoomMap.js
|
||||
src/utils/DecryptFile.js
|
||||
src/utils/DirectoryUtils.js
|
||||
src/utils/MultiInviter.js
|
||||
src/utils/Receipt.js
|
||||
test/components/structures/MessagePanel-test.js
|
||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||
test/mock-clock.js
|
||||
test/notifications/ContentRules-test.js
|
||||
test/notifications/PushRuleVectorState-test.js
|
||||
src/component-index.js
|
||||
test/end-to-end-tests/node_modules/
|
||||
test/end-to-end-tests/riot/
|
||||
|
|
256
CHANGELOG.md
|
@ -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)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0...v3.6.1)
|
||||
|
|
|
@ -18,7 +18,7 @@ are currently filed against vector-im/element-web rather than this project).
|
|||
|
||||
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
|
||||
===============
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "matrix-react-sdk",
|
||||
"version": "3.6.1",
|
||||
"version": "3.9.0",
|
||||
"description": "SDK for matrix.org using React",
|
||||
"author": "matrix.org",
|
||||
"repository": {
|
||||
|
@ -79,7 +79,7 @@
|
|||
"linkifyjs": "^2.1.9",
|
||||
"lodash": "^4.17.19",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^0.1.0-beta.5",
|
||||
"matrix-widget-api": "^0.1.0-beta.10",
|
||||
"minimist": "^1.2.5",
|
||||
"pako": "^1.0.11",
|
||||
"parse5": "^5.1.1",
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
@import "./_font-sizes.scss";
|
||||
@import "./_font-weights.scss";
|
||||
|
||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
||||
|
||||
|
@ -59,6 +60,10 @@ pre, code {
|
|||
color: $accent-color;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: $muted-fg-color;
|
||||
}
|
||||
|
||||
b {
|
||||
// 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.
|
||||
|
@ -323,6 +328,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
|
||||
.mx_Dialog_title {
|
||||
font-size: $font-22px;
|
||||
font-weight: $font-semi-bold;
|
||||
line-height: $font-36px;
|
||||
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;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 0px;
|
||||
top: 10px;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.mx_Dialog_content {
|
||||
|
@ -362,6 +368,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
.mx_Dialog_buttons {
|
||||
margin-top: 20px;
|
||||
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
|
||||
|
|
|
@ -9,10 +9,12 @@
|
|||
@import "./structures/_CustomRoomTagPanel.scss";
|
||||
@import "./structures/_FilePanel.scss";
|
||||
@import "./structures/_GenericErrorPage.scss";
|
||||
@import "./structures/_GroupFilterPanel.scss";
|
||||
@import "./structures/_GroupView.scss";
|
||||
@import "./structures/_HeaderButtons.scss";
|
||||
@import "./structures/_HomePage.scss";
|
||||
@import "./structures/_LeftPanel.scss";
|
||||
@import "./structures/_LeftPanelWidget.scss";
|
||||
@import "./structures/_MainSplit.scss";
|
||||
@import "./structures/_MatrixChat.scss";
|
||||
@import "./structures/_MyGroups.scss";
|
||||
|
@ -26,7 +28,6 @@
|
|||
@import "./structures/_ScrollPanel.scss";
|
||||
@import "./structures/_SearchBox.scss";
|
||||
@import "./structures/_TabbedView.scss";
|
||||
@import "./structures/_GroupFilterPanel.scss";
|
||||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
@import "./structures/_UserMenu.scss";
|
||||
|
@ -69,11 +70,13 @@
|
|||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||
@import "./views/dialogs/_InviteDialog.scss";
|
||||
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
||||
|
@ -88,6 +91,7 @@
|
|||
@import "./views/dialogs/_TermsDialog.scss";
|
||||
@import "./views/dialogs/_UploadConfirmDialog.scss";
|
||||
@import "./views/dialogs/_UserSettingsDialog.scss";
|
||||
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
|
||||
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
|
||||
@import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
|
||||
@import "./views/dialogs/security/_CreateCrossSigningDialog.scss";
|
||||
|
@ -107,11 +111,11 @@
|
|||
@import "./views/elements/_EventListSummary.scss";
|
||||
@import "./views/elements/_Field.scss";
|
||||
@import "./views/elements/_FormButton.scss";
|
||||
@import "./views/elements/_IconButton.scss";
|
||||
@import "./views/elements/_ImageView.scss";
|
||||
@import "./views/elements/_InfoTooltip.scss";
|
||||
@import "./views/elements/_InlineSpinner.scss";
|
||||
@import "./views/elements/_ManageIntegsButton.scss";
|
||||
@import "./views/elements/_MiniAvatarUploader.scss";
|
||||
@import "./views/elements/_PowerSelector.scss";
|
||||
@import "./views/elements/_ProgressBar.scss";
|
||||
@import "./views/elements/_QRCode.scss";
|
||||
|
@ -136,6 +140,7 @@
|
|||
@import "./views/groups/_GroupUserSettings.scss";
|
||||
@import "./views/messages/_CreateEvent.scss";
|
||||
@import "./views/messages/_DateSeparator.scss";
|
||||
@import "./views/messages/_EventTileBubble.scss";
|
||||
@import "./views/messages/_MEmoteBody.scss";
|
||||
@import "./views/messages/_MFileBody.scss";
|
||||
@import "./views/messages/_MImageBody.scss";
|
||||
|
@ -179,6 +184,7 @@
|
|||
@import "./views/rooms/_MemberList.scss";
|
||||
@import "./views/rooms/_MessageComposer.scss";
|
||||
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
||||
@import "./views/rooms/_NewRoomIntro.scss";
|
||||
@import "./views/rooms/_NotificationBadge.scss";
|
||||
@import "./views/rooms/_PinnedEventTile.scss";
|
||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||
|
@ -222,8 +228,9 @@
|
|||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||
@import "./views/toasts/_AnalyticsToast.scss";
|
||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||
@import "./views/verification/_VerificationShowSas.scss";
|
||||
@import "./views/voip/_CallContainer.scss";
|
||||
@import "./views/voip/_CallView.scss";
|
||||
@import "./views/voip/_VideoView.scss";
|
||||
@import "./views/voip/_VideoFeed.scss";
|
||||
|
|
|
@ -16,11 +16,6 @@ limitations under the License.
|
|||
|
||||
// TODO: Update design for custom tags to match new designs
|
||||
|
||||
.mx_LeftPanel_tagPanelContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mx_CustomRoomTagPanel {
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
max-height: 40vh;
|
||||
|
|
|
@ -26,9 +26,10 @@ limitations under the License.
|
|||
|
||||
.mx_HomePage_default {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
|
||||
.mx_HomePage_default_wrapper {
|
||||
padding: 25vh 0 12px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
img {
|
||||
|
@ -50,56 +51,54 @@ limitations under the License.
|
|||
color: $muted-fg-color;
|
||||
}
|
||||
|
||||
.mx_MiniAvatarUploader {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.mx_HomePage_default_buttons {
|
||||
margin: 80px auto 0;
|
||||
margin: 60px auto 0;
|
||||
width: fit-content;
|
||||
|
||||
.mx_AccessibleButton {
|
||||
padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin
|
||||
|
||||
width: 104px; // 120px - 2* 8px
|
||||
margin: 0 39px; // 55px - 2* 8px
|
||||
width: 160px;
|
||||
height: 132px;
|
||||
margin: 20px;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-radius: 8px;
|
||||
vertical-align: top;
|
||||
word-break: break-word;
|
||||
box-sizing: border-box;
|
||||
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-20px;
|
||||
color: $muted-fg-color;
|
||||
|
||||
&:hover {
|
||||
color: $accent-color;
|
||||
background: rgba($accent-color, 0.06);
|
||||
|
||||
&::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
color: #fff; // on all themes
|
||||
background-color: $accent-color;
|
||||
|
||||
&::before {
|
||||
top: 20px;
|
||||
left: 40px; // (120px-40px)/2
|
||||
left: 60px; // (160px-40px)/2
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: $muted-fg-color;
|
||||
background-color: #fff; // on all themes
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
}
|
||||
|
||||
&.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 {
|
||||
mask-image: url('$(res)/img/feather-customised/explore.svg');
|
||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||
}
|
||||
&.mx_HomePage_button_createGroup::before {
|
||||
mask-image: url('$(res)/img/feather-customised/group.svg');
|
||||
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ $groupFilterPanelWidth: 56px; // only applies in this file, used for calculation
|
|||
|
||||
// Create another flexbox so the GroupFilterPanel fills the container
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// GroupFilterPanel handles its own CSS
|
||||
}
|
||||
|
|
145
res/css/structures/_LeftPanelWidget.scss
Normal 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;
|
||||
}
|
|
@ -19,57 +19,6 @@ limitations under the License.
|
|||
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 {
|
||||
width: 52px;
|
||||
margin-top: -1px;
|
||||
|
@ -153,16 +102,6 @@ limitations under the License.
|
|||
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_RoomStatusBar {
|
||||
min-height: 40px;
|
||||
|
@ -172,11 +111,6 @@ limitations under the License.
|
|||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_callBar {
|
||||
height: 40px;
|
||||
line-height: $font-40px;
|
||||
}
|
||||
|
||||
.mx_RoomStatusBar_typingBar {
|
||||
height: 40px;
|
||||
line-height: $font-40px;
|
||||
|
|
|
@ -14,6 +14,35 @@ See the License for the specific language governing permissions and
|
|||
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 {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ limitations under the License.
|
|||
|
||||
.mx_BaseAvatar_image {
|
||||
object-fit: cover;
|
||||
border-radius: 40px;
|
||||
border-radius: 125px;
|
||||
vertical-align: top;
|
||||
background-color: $avatar-bg-color;
|
||||
}
|
||||
|
|
121
res/css/views/dialogs/_FeedbackDialog.scss
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -27,37 +27,29 @@ limitations under the License.
|
|||
padding-left: 8px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.mx_InviteDialog_userTile {
|
||||
margin: 6px 6px 0 0;
|
||||
display: inline-block;
|
||||
float: left;
|
||||
position: relative;
|
||||
top: 7px;
|
||||
min-width: max-content; // prevent manipulation by flexbox
|
||||
}
|
||||
|
||||
// Using a textarea for this element, to circumvent autofill
|
||||
// Mostly copied from AddressPickerDialog
|
||||
textarea,
|
||||
textarea:focus {
|
||||
height: 34px;
|
||||
line-height: $font-34px;
|
||||
// Mostly copied from AddressPickerDialog; overrides bunch of our default text input styles
|
||||
> input[type="text"] {
|
||||
margin: 6px 0 !important;
|
||||
height: 24px;
|
||||
line-height: $font-24px;
|
||||
font-size: $font-14px;
|
||||
padding-left: 12px;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
outline: 0 !important;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
word-wrap: nowrap;
|
||||
|
||||
// Roughly fill about 2/5ths of the available space. This is to try and 'fill' the
|
||||
// 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%;
|
||||
min-width: 40%;
|
||||
flex: 1 !important;
|
||||
color: $primary-fg-color !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,6 +140,10 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_InviteDialog_roomTile_nameStack {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_InviteDialog_roomTile_name {
|
||||
font-weight: 600;
|
||||
font-size: $font-14px;
|
||||
|
|
42
res/css/views/dialogs/_ModalWidgetDialog.scss
Normal 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;
|
||||
}
|
||||
}
|
75
res/css/views/dialogs/_WidgetCapabilitiesPromptDialog.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,7 @@ limitations under the License.
|
|||
.mx_AccessibleButton_hasKind {
|
||||
padding: 7px 18px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
font-size: $font-14px;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
56
res/css/views/elements/_MiniAvatarUploader.scss
Normal 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;
|
||||
}
|
|
@ -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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,25 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_CreateEvent {
|
||||
background-color: $info-plinth-bg-color;
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
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;
|
||||
&::before {
|
||||
background-color: $composer-e2e-icon-color;
|
||||
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
|
||||
}
|
||||
}
|
||||
|
|
60
res/css/views/messages/_EventTileBubble.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -15,41 +15,8 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.mx_MJitsiWidgetEvent {
|
||||
display: grid;
|
||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||
|
||||
&::before {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 3;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
content: "";
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
||||
margin-top: 4px;
|
||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_title {
|
||||
font-weight: 600;
|
||||
font-size: $font-15px;
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_subtitle {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.mx_MJitsiWidgetEvent_title,
|
||||
.mx_MJitsiWidgetEvent_subtitle {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,5 +18,6 @@ span.mx_MVideoBody {
|
|||
video.mx_MVideoBody {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,28 +15,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
.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
|
||||
&.mx_cryptoEvent_icon::before {
|
||||
background-color: #ffffff;
|
||||
|
@ -46,6 +24,11 @@ limitations under the License.
|
|||
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 {
|
||||
mask-image: url("$(res)/img/e2e/verified.svg");
|
||||
background-color: $accent-color;
|
||||
|
@ -56,25 +39,6 @@ limitations under the License.
|
|||
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 {
|
||||
grid-column: 3;
|
||||
|
@ -92,5 +56,7 @@ limitations under the License.
|
|||
margin: auto 0;
|
||||
text-align: center;
|
||||
color: $notice-secondary-color;
|
||||
overflow-wrap: break-word;
|
||||
font-size: $font-12px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -173,26 +173,12 @@ limitations under the License.
|
|||
|
||||
margin: 6px 0;
|
||||
|
||||
.mx_IconButton, .mx_Spinner {
|
||||
margin-left: 20px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&::before {
|
||||
mask-size: 80%;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_UserInfo_roleDescription {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
// try to make it the same height as the dropdown
|
||||
margin: 11px 0 12px 0;
|
||||
|
||||
.mx_IconButton {
|
||||
margin-left: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_Field {
|
||||
|
|
|
@ -25,15 +25,6 @@ $left-gutter: 64px;
|
|||
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 {
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
@ -131,9 +122,10 @@ $left-gutter: 64px;
|
|||
grid-template-columns: 1fr 100px;
|
||||
|
||||
.mx_EventTile_line {
|
||||
margin-right: 0px;
|
||||
margin-right: 0;
|
||||
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 {
|
||||
|
|
|
@ -186,6 +186,7 @@ $irc-line-height: $font-18px;
|
|||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: var(--name-width);
|
||||
text-align: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
67
res/css/views/rooms/_NewRoomIntro.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -33,7 +33,6 @@ limitations under the License.
|
|||
|
||||
div:first-child {
|
||||
font-weight: $font-semi-bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton {
|
||||
|
@ -41,6 +40,7 @@ limitations under the License.
|
|||
position: relative;
|
||||
padding: 0 0 0 24px;
|
||||
font-size: inherit;
|
||||
margin-top: 8px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
@ -53,6 +53,13 @@ limitations under the License.
|
|||
mask-position: center;
|
||||
mask-size: contain;
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,10 +59,6 @@ limitations under the License.
|
|||
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
|
||||
// height, and is therefore calculated in JS.
|
||||
// The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
|
||||
|
@ -387,3 +383,22 @@ limitations under the License.
|
|||
.mx_RoomSublist_addRoomTooltip {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,15 +14,14 @@ See the License for the specific language governing permissions and
|
|||
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;
|
|
@ -33,11 +33,11 @@ limitations under the License.
|
|||
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||
cursor: pointer;
|
||||
|
||||
.mx_VideoView {
|
||||
.mx_CallView_video {
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.mx_VideoView_localVideoFeed {
|
||||
.mx_VideoFeed_local {
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
@ -15,80 +15,196 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_CallView_voice {
|
||||
background-color: $accent-color;
|
||||
color: $accent-fg-color;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
font-weight: bold;
|
||||
.mx_CallView {
|
||||
border-radius: 10px;
|
||||
background-color: $input-lighter-bg-color;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
.mx_CallView_large {
|
||||
padding-bottom: 10px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
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_voice {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_hangup {
|
||||
position: absolute;
|
||||
.mx_CallView_pip {
|
||||
width: 320px;
|
||||
|
||||
right: 8px;
|
||||
bottom: 10px;
|
||||
.mx_CallView_voice {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
.mx_CallView_voice {
|
||||
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;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
|
||||
top: 6.5px;
|
||||
left: 7.5px;
|
||||
|
||||
mask: url('$(res)/img/hangup.svg');
|
||||
vertical-align: middle;
|
||||
background-color: $secondary-fg-color;
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: contain;
|
||||
background-size: contain;
|
||||
|
||||
background-color: $primary-fg-color;
|
||||
mask-position: center;
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
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.
|
||||
*/
|
||||
|
||||
.mx_VideoView {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
.mx_VideoView video {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_VideoView_remoteVideoFeed {
|
||||
.mx_VideoFeed_remote {
|
||||
width: 100%;
|
||||
background-color: #000;
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.mx_VideoView_localVideoFeed {
|
||||
.mx_VideoFeed_local {
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
top: 10px;
|
||||
z-index: 100;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mx_VideoView_localVideoFeed video {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
|
||||
.mx_VideoFeed_mirror {
|
||||
transform: scale(-1, 1);
|
||||
}
|
3
res/img/element-icons/call/expand.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path 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 |
|
@ -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 |
|
@ -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 |
|
@ -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 |
10
res/img/element-icons/camera.svg
Normal 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 |
11
res/img/element-icons/chat-bubbles.svg
Normal 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 |
13
res/img/element-icons/email-prompt.svg
Normal 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 |
3
res/img/element-icons/feedback.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="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 |
|
@ -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 |
5
res/img/element-icons/roomlist/skeleton-ui.svg
Normal 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 |
5
res/img/element-icons/warning-badge.svg
Normal 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 |
3
res/img/feather-customised/bug.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="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 |
|
@ -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 |
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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 |
|
@ -117,6 +117,7 @@ $roomlist-filter-active-bg-color: $bg-color;
|
|||
$roomlist-bg-color: rgba(33, 38, 44, 0.90);
|
||||
$roomlist-header-color: $tertiary-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;
|
||||
|
||||
|
@ -273,6 +274,10 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
|
|||
background-color: #080808;
|
||||
}
|
||||
}
|
||||
|
||||
blockquote {
|
||||
color: #919191;
|
||||
}
|
||||
}
|
||||
|
||||
// diff highlight colors
|
||||
|
|
|
@ -114,6 +114,7 @@ $roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
|||
$roomlist-bg-color: $header-panel-bg-color;
|
||||
|
||||
$roomsublist-divider-color: $primary-fg-color;
|
||||
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
|
|
|
@ -181,6 +181,7 @@ $roomlist-filter-active-bg-color: $roomlist-button-bg-color;
|
|||
$roomlist-bg-color: $header-panel-bg-color;
|
||||
$roomlist-header-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;
|
||||
|
||||
|
|
|
@ -175,6 +175,7 @@ $roomlist-filter-active-bg-color: #ffffff;
|
|||
$roomlist-bg-color: rgba(245, 245, 245, 0.90);
|
||||
$roomlist-header-color: $tertiary-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;
|
||||
|
||||
|
|
27
src/@types/global.d.ts
vendored
|
@ -33,7 +33,9 @@ import RightPanelStore from "../stores/RightPanelStore";
|
|||
import WidgetStore from "../stores/WidgetStore";
|
||||
import CallHandler from "../CallHandler";
|
||||
import {Analytics} from "../Analytics";
|
||||
import CountlyAnalytics from "../CountlyAnalytics";
|
||||
import UserActivity from "../UserActivity";
|
||||
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -59,12 +61,21 @@ declare global {
|
|||
mxWidgetStore: WidgetStore;
|
||||
mxCallHandler: CallHandler;
|
||||
mxAnalytics: Analytics;
|
||||
mxCountlyAnalytics: typeof CountlyAnalytics;
|
||||
mxUserActivity: UserActivity;
|
||||
mxModalWidgetStore: ModalWidgetStore;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
||||
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 {
|
||||
|
@ -94,4 +105,20 @@ declare global {
|
|||
interface HTMLAudioElement {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import {ActionPayload} from "./dispatcher/payloads";
|
|||
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
||||
import {Action} from "./dispatcher/actions";
|
||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
|
||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_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
|
||||
*/
|
||||
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 {
|
||||
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
|
||||
return newVersion !== version || Date.now() > deferUntil;
|
||||
|
|
|
@ -59,8 +59,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||
import PlatformPeg from './PlatformPeg';
|
||||
import Modal from './Modal';
|
||||
import { _t } from './languageHandler';
|
||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import WidgetUtils from './utils/WidgetUtils';
|
||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||
|
@ -77,8 +76,10 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
|||
import WidgetStore from "./stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/lib/webrtc/call";
|
||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||
import Analytics from './Analytics';
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
|
@ -97,6 +98,21 @@ export enum PlaceCallType {
|
|||
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 {
|
||||
private calls = new Map<string, MatrixCall>();
|
||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||
|
@ -109,7 +125,7 @@ export default class CallHandler {
|
|||
return window.mxCallHandler;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
start() {
|
||||
dis.register(this.onAction);
|
||||
// add empty handlers for media actions, otherwise the media keys
|
||||
// end up causing the audio elements with our ring/ringback etc
|
||||
|
@ -122,6 +138,27 @@ export default class CallHandler {
|
|||
navigator.mediaSession.setActionHandler('previoustrack', 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 {
|
||||
|
@ -262,6 +299,12 @@ export default class CallHandler {
|
|||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
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 {
|
||||
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) {
|
||||
console.log(
|
||||
`Call state in ${call.roomId} changed to ${status}`,
|
||||
|
@ -335,9 +383,12 @@ export default class CallHandler {
|
|||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
) {
|
||||
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.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
|
||||
if (type === PlaceCallType.Voice) {
|
||||
call.placeVoiceCall();
|
||||
} else if (type === 'video') {
|
||||
|
@ -413,6 +464,7 @@ export default class CallHandler {
|
|||
case 'place_conference_call':
|
||||
console.info("Place conference call in %s", payload.room_id);
|
||||
Analytics.trackEvent('voip', 'placeConferenceCall');
|
||||
CountlyAnalytics.instance.trackStartCall(payload.room_id, payload.type === PlaceCallType.Video, true);
|
||||
this.startCallApp(payload.room_id, payload.type);
|
||||
break;
|
||||
case 'end_conference':
|
||||
|
@ -442,6 +494,7 @@ export default class CallHandler {
|
|||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
this.calls.set(call.roomId, call)
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
}
|
||||
break;
|
||||
case 'hangup':
|
||||
|
@ -456,16 +509,19 @@ export default class CallHandler {
|
|||
}
|
||||
this.removeCallForRoom(payload.room_id);
|
||||
break;
|
||||
case 'answer':
|
||||
case 'answer': {
|
||||
if (!this.calls.has(payload.room_id)) {
|
||||
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({
|
||||
action: "view_room",
|
||||
room_id: payload.room_id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ import Spinner from "./components/views/elements/Spinner";
|
|||
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||
import "blueimp-canvas-to-blob";
|
||||
import { Action } from "./dispatcher/actions";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const MAX_HEIGHT = 600;
|
||||
|
@ -368,10 +369,13 @@ export default class ContentMessages {
|
|||
private mediaConfig: IMediaConfig = null;
|
||||
|
||||
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);
|
||||
throw e;
|
||||
});
|
||||
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, {msgtype: "m.sticker"});
|
||||
return prom;
|
||||
}
|
||||
|
||||
getUploadLimit() {
|
||||
|
@ -479,6 +483,7 @@ export default class ContentMessages {
|
|||
}
|
||||
|
||||
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
|
||||
const startTime = CountlyAnalytics.getTimestamp();
|
||||
const content: IContent = {
|
||||
body: file.name || 'Attachment',
|
||||
info: {
|
||||
|
@ -563,7 +568,9 @@ export default class ContentMessages {
|
|||
return promBefore;
|
||||
}).then(function() {
|
||||
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) {
|
||||
error = err;
|
||||
if (!upload.canceled) {
|
||||
|
|
973
src/CountlyAnalytics.ts
Normal 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;
|
|
@ -29,6 +29,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
|
|||
import url from 'url';
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import SettingsStore from './settings/SettingsStore';
|
||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||
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
|
||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||
// 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: {}};
|
||||
}
|
||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||
|
|
|
@ -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");
|
||||
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.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Returns the actual height that an image of dimensions (fullWidth, fullHeight)
|
||||
* 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
|
||||
* (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) {
|
||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||
// log this because it's spammy
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
||||
// no scaling needs to be applied
|
|
@ -47,6 +47,8 @@ import DeviceListener from "./DeviceListener";
|
|||
import {Jitsi} from "./widgets/Jitsi";
|
||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||
import CountlyAnalytics from "./CountlyAnalytics";
|
||||
import CallHandler from './CallHandler';
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
@ -580,6 +582,10 @@ let _isLoggingOut = false;
|
|||
*/
|
||||
export function logout(): void {
|
||||
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()) {
|
||||
// logout doesn't work for guest sessions
|
||||
|
@ -660,6 +666,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
|
|||
DMRoomMap.makeShared().start();
|
||||
IntegrationManagers.sharedInstance().startWatching();
|
||||
ActiveWidgetStore.start();
|
||||
CallHandler.sharedInstance().start();
|
||||
|
||||
// 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
|
||||
|
@ -755,6 +762,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
|
|||
*/
|
||||
export function stopMatrixClient(unsetClient = true): void {
|
||||
Notifier.stop();
|
||||
CallHandler.sharedInstance().stop();
|
||||
UserActivity.sharedInstance().stop();
|
||||
TypingStore.sharedInstance().reset();
|
||||
Presence.stop();
|
||||
|
|
|
@ -34,6 +34,7 @@ import * as StorageManager from './utils/StorageManager';
|
|||
import IdentityAuthClient from './IdentityAuthClient';
|
||||
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
||||
export interface IMatrixClientCreds {
|
||||
homeserverUrl: string;
|
||||
|
@ -100,6 +101,12 @@ export interface IMatrixClientPeg {
|
|||
*/
|
||||
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
|
||||
* homeserver / identity server URLs and active credentials
|
||||
|
@ -150,6 +157,9 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
|||
|
||||
public setJustRegisteredUserId(uid: string): void {
|
||||
this.justRegisteredUserId = uid;
|
||||
if (uid) {
|
||||
window.localStorage.setItem("mx_registration_time", String(new Date().getTime()));
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
this.currentClientCreds = creds;
|
||||
this.createClient(creds);
|
||||
|
@ -273,7 +292,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
|||
// These are always installed regardless of the labs flag so that
|
||||
// cross-signing features can toggle on without reloading and also be
|
||||
// accessed immediately after login.
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
||||
const customisedCallbacks = {
|
||||
getDehydrationKey: SecurityCustomisations.getDehydrationKey,
|
||||
};
|
||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks);
|
||||
|
||||
this.matrixClient = createMatrixClient(opts);
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
|
|||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
||||
|
||||
interface IModal<T extends any[]> {
|
||||
export interface IModal<T extends any[]> {
|
||||
elem: React.ReactNode;
|
||||
className?: string;
|
||||
beforeClosePromise?: Promise<boolean>;
|
||||
|
@ -38,7 +38,7 @@ interface IModal<T extends any[]> {
|
|||
close(...args: T): void;
|
||||
}
|
||||
|
||||
interface IHandle<T extends any[]> {
|
||||
export interface IHandle<T extends any[]> {
|
||||
finished: Promise<T>;
|
||||
close(...args: T): void;
|
||||
}
|
||||
|
@ -147,6 +147,15 @@ export class ModalManager {
|
|||
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[]>(
|
||||
prom: Promise<React.ComponentType>,
|
||||
props?: IProps<T>,
|
||||
|
|
|
@ -34,6 +34,8 @@ import SettingsStore from "./settings/SettingsStore";
|
|||
import { hideToast as hideNotificationsToast } from "./toasts/DesktopNotificationsToast";
|
||||
import {SettingLevel} from "./settings/SettingLevel";
|
||||
import {isPushNotifyDisabled} from "./settings/controllers/NotificationControllers";
|
||||
import RoomViewStore from "./stores/RoomViewStore";
|
||||
import UserActivity from "./UserActivity";
|
||||
|
||||
/*
|
||||
* Dispatches:
|
||||
|
@ -376,6 +378,11 @@ export const Notifier = {
|
|||
const room = MatrixClientPeg.get().getRoom(ev.getRoomId());
|
||||
const actions = MatrixClientPeg.get().getPushActionsForEvent(ev);
|
||||
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()) {
|
||||
this._displayPopupNotification(ev, room);
|
||||
}
|
||||
|
|
|
@ -40,11 +40,11 @@ export function inviteMultipleToRoom(roomId, addrs) {
|
|||
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.
|
||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||
Modal.createTrackedDialog(
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
||||
'Start DM', '', InviteDialog, {kind: KIND_DM, initialText},
|
||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -21,6 +21,9 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||
* if any. This could be the canonical alias if one exists, otherwise
|
||||
* an alias selected arbitrarily but deterministically from the list
|
||||
* of aliases. Otherwise return null;
|
||||
*
|
||||
* @param {Object} room The room object
|
||||
* @returns {string} A display alias for the given room
|
||||
*/
|
||||
export function getDisplayAliasForRoom(room) {
|
||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||
|
|
|
@ -50,8 +50,8 @@ class Skinner {
|
|||
return null;
|
||||
}
|
||||
|
||||
// components have to be functions.
|
||||
const validType = typeof comp === 'function';
|
||||
// components have to be functions or forwardRef objects with a render function.
|
||||
const validType = typeof comp === 'function' || comp.render;
|
||||
if (!validType) {
|
||||
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@ import SdkConfig from "./SdkConfig";
|
|||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import effects from "./components/views/elements/effects"
|
||||
import CallHandler from "./CallHandler";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -519,6 +520,7 @@ export const Commands = [
|
|||
action: 'view_room',
|
||||
room_alias: roomAlias,
|
||||
auto_join: true,
|
||||
_type: "slash_command", // instrumentation
|
||||
});
|
||||
return success();
|
||||
} else if (params[0][0] === '!') {
|
||||
|
@ -533,6 +535,7 @@ export const Commands = [
|
|||
},
|
||||
via_servers: viaServers, // for the rejoin button
|
||||
auto_join: true,
|
||||
_type: "slash_command", // instrumentation
|
||||
});
|
||||
return success();
|
||||
} else if (isPermalink) {
|
||||
|
@ -557,6 +560,7 @@ export const Commands = [
|
|||
const dispatch = {
|
||||
action: 'view_room',
|
||||
auto_join: true,
|
||||
_type: "slash_command", // instrumentation
|
||||
};
|
||||
|
||||
if (entity[0] === '!') dispatch["room_id"] = entity;
|
||||
|
@ -1000,14 +1004,29 @@ export const Commands = [
|
|||
description: _td("Opens chat with the given user"),
|
||||
args: "<user-id>",
|
||||
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 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({
|
||||
action: 'view_room',
|
||||
room_id: await ensureDMExists(MatrixClientPeg.get(), userId),
|
||||
room_id: roomId,
|
||||
});
|
||||
})());
|
||||
},
|
||||
|
@ -1041,6 +1060,43 @@ export const Commands = [
|
|||
},
|
||||
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) => {
|
||||
return new Command({
|
||||
command: effect.command,
|
||||
|
@ -1064,16 +1120,6 @@ export const Commands = [
|
|||
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.
|
||||
|
|
|
@ -455,7 +455,7 @@ function textForWidgetEvent(event) {
|
|||
let widgetName = name || prevName || type || prevType || '';
|
||||
// Apply sentence case to widget name
|
||||
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
|
||||
|
|
|
@ -16,12 +16,14 @@ limitations under the License.
|
|||
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import shouldHideEvent from './shouldHideEvent';
|
||||
import * as sdk from "./index";
|
||||
import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
||||
|
||||
/**
|
||||
* Returns true iff this event arriving in a room should affect the room's
|
||||
* 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) {
|
||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||
|
|
|
@ -257,6 +257,12 @@ const shortcuts: Record<Categories, IShortcut[]> = {
|
|||
key: Key.SLASH,
|
||||
}],
|
||||
description: _td("Toggle this dialog"),
|
||||
}, {
|
||||
keybinds: [{
|
||||
modifiers: [CMD_OR_CTRL, Modifiers.ALT],
|
||||
key: Key.H,
|
||||
}],
|
||||
description: _td("Go to Home View"),
|
||||
},
|
||||
],
|
||||
|
||||
|
|
|
@ -205,7 +205,7 @@ export const RovingTabIndexProvider: React.FC<IProps> = ({children, handleHomeEn
|
|||
// 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}`
|
||||
// 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);
|
||||
let ref = useRef<HTMLElement>(null);
|
||||
|
||||
|
|
|
@ -470,6 +470,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
value={CREATE_STORAGE_OPTION_KEY}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
||||
onChange={this._onKeyPassphraseChange}
|
||||
outlined
|
||||
>
|
||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||
|
@ -488,6 +489,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
name="keyPassphrase"
|
||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||
onChange={this._onKeyPassphraseChange}
|
||||
outlined
|
||||
>
|
||||
<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 " +
|
||||
"backing up encryption keys on your server.",
|
||||
)}</p>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup" onChange={this._onKeyPassphraseChange}>
|
||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
|
||||
{optionKey}
|
||||
{optionPassphrase}
|
||||
</div>
|
||||
|
|
|
@ -47,7 +47,7 @@ const LONG_DESC_PLACEHOLDER = _td(
|
|||
some important <a href="foo">links</a>
|
||||
</p>
|
||||
<p>
|
||||
You can even use 'img' tags
|
||||
You can even add images with Matrix URLs <img src="mxc://url" />
|
||||
</p>
|
||||
`);
|
||||
|
||||
|
|
|
@ -15,20 +15,83 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import * as React from "react";
|
||||
import {useContext, useState} from "react";
|
||||
|
||||
import AutoHideScrollbar from './AutoHideScrollbar';
|
||||
import { getHomePageUrl } from "../../utils/pages";
|
||||
import { _t } from "../../languageHandler";
|
||||
import {getHomePageUrl} from "../../utils/pages";
|
||||
import {_t} from "../../languageHandler";
|
||||
import SdkConfig from "../../SdkConfig";
|
||||
import * as sdk from "../../index";
|
||||
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 onClickExplore = () => dis.fire(Action.ViewRoomDirectory);
|
||||
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'});
|
||||
const onClickSendDm = () => {
|
||||
Analytics.trackEvent('home_page', 'button', 'dm');
|
||||
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 pageUrl = getHomePageUrl(config);
|
||||
|
||||
|
@ -37,18 +100,27 @@ const HomePage = () => {
|
|||
return <EmbeddedPage className="mx_HomePage" url={pageUrl} scrollbar={true} />;
|
||||
}
|
||||
|
||||
const brandingConfig = config.branding;
|
||||
let logoUrl = "themes/element/img/logos/element-logo.svg";
|
||||
if (brandingConfig && brandingConfig.authHeaderLogoUrl) {
|
||||
logoUrl = brandingConfig.authHeaderLogoUrl;
|
||||
let introSection;
|
||||
if (justRegistered) {
|
||||
introSection = <UserWelcomeTop />;
|
||||
} 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">
|
||||
<div className="mx_HomePage_default_wrapper">
|
||||
<img src={logoUrl} alt={config.brand || "Element"} />
|
||||
<h1>{ _t("Welcome to %(appName)s", { appName: config.brand || "Element" }) }</h1>
|
||||
<h4>{ _t("Liberate your communication") }</h4>
|
||||
{ introSection }
|
||||
<div className="mx_HomePage_default_buttons">
|
||||
<AccessibleButton onClick={onClickSendDm} className="mx_HomePage_button_sendDm">
|
||||
{ _t("Send a Direct Message") }
|
||||
|
|
|
@ -38,6 +38,7 @@ import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
|||
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||
import RoomListNumResults from "../views/rooms/RoomListNumResults";
|
||||
import LeftPanelWidget from "./LeftPanelWidget";
|
||||
|
||||
interface IProps {
|
||||
isMinimized: boolean;
|
||||
|
@ -142,7 +143,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
const bottomEdge = list.offsetHeight + list.scrollTop;
|
||||
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;
|
||||
|
||||
// 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")) {
|
||||
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 {
|
||||
if (header.classList.contains("mx_RoomSublist_headerContainer_stickyBottom")) {
|
||||
header.classList.remove("mx_RoomSublist_headerContainer_stickyBottom");
|
||||
}
|
||||
if (header.style.bottom) {
|
||||
header.style.removeProperty('bottom');
|
||||
}
|
||||
}
|
||||
|
||||
if (style.stickyTop || style.stickyBottom) {
|
||||
|
@ -425,6 +435,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
|||
{roomList}
|
||||
</div>
|
||||
</div>
|
||||
{ !this.props.isMinimized && <LeftPanelWidget onResize={this.onResize} /> }
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
|
|
149
src/components/structures/LeftPanelWidget.tsx
Normal 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;
|
|
@ -21,7 +21,7 @@ import * as PropTypes from 'prop-types';
|
|||
import { MatrixClient } from 'matrix-js-sdk/src/client';
|
||||
import { DragDropContext } from 'react-beautiful-dnd';
|
||||
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent} from '../../Keyboard';
|
||||
import {Key, isOnlyCtrlOrCmdKeyEvent, isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isMac} from '../../Keyboard';
|
||||
import PageTypes from '../../PageTypes';
|
||||
import CallMediaHandler from '../../CallMediaHandler';
|
||||
import { fixupColorFonts } from '../../utils/FontManager';
|
||||
|
@ -52,6 +52,7 @@ import RoomListStore from "../../stores/room-list/RoomListStore";
|
|||
import NonUrgentToastContainer from "./NonUrgentToastContainer";
|
||||
import { ToggleRightPanelPayload } from "../../dispatcher/payloads/ToggleRightPanelPayload";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import Modal from "../../Modal";
|
||||
import { ICollapseConfig } from "../../resizer/distributors/collapse";
|
||||
|
||||
// We need to fetch each pinned message individually (if we don't already have it)
|
||||
|
@ -88,6 +89,7 @@ interface IProps {
|
|||
currentUserId?: string;
|
||||
currentGroupId?: string;
|
||||
currentGroupIsNew?: boolean;
|
||||
justRegistered?: boolean;
|
||||
}
|
||||
|
||||
interface IUsageLimit {
|
||||
|
@ -391,6 +393,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
|
||||
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 modKey = isMac ? ev.metaKey : ev.ctrlKey;
|
||||
|
||||
switch (ev.key) {
|
||||
case Key.PAGE_UP:
|
||||
|
@ -435,6 +438,16 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
}
|
||||
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_DOWN:
|
||||
if (ev.altKey && !ev.ctrlKey && !ev.metaKey) {
|
||||
|
@ -573,7 +586,7 @@ class LoggedInView extends React.Component<IProps, IState> {
|
|||
break;
|
||||
|
||||
case PageTypes.HomePage:
|
||||
pageElement = <HomePage />;
|
||||
pageElement = <HomePage justRegistered={this.props.justRegistered} />;
|
||||
break;
|
||||
|
||||
case PageTypes.UserView:
|
||||
|
|
|
@ -29,6 +29,7 @@ import 'focus-visible';
|
|||
import 'what-input';
|
||||
|
||||
import Analytics from "../../Analytics";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
import { DecryptionFailureTracker } from "../../DecryptionFailureTracker";
|
||||
import { MatrixClientPeg, IMatrixClientCreds } from "../../MatrixClientPeg";
|
||||
import PlatformPeg from "../../PlatformPeg";
|
||||
|
@ -61,7 +62,7 @@ import DMRoomMap from '../../utils/DMRoomMap';
|
|||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||
import { defer, IDeferred } from "../../utils/promise";
|
||||
import { defer, IDeferred, sleep } from "../../utils/promise";
|
||||
import ToastStore from "../../stores/ToastStore";
|
||||
import * as StorageManager from "../../utils/StorageManager";
|
||||
import type LoggedInViewType from "./LoggedInView";
|
||||
|
@ -86,37 +87,37 @@ import { CommunityPrototypeStore } from "../../stores/CommunityPrototypeStore";
|
|||
export enum Views {
|
||||
// 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.
|
||||
LOADING = 0,
|
||||
LOADING,
|
||||
|
||||
// we are showing the welcome view
|
||||
WELCOME = 1,
|
||||
WELCOME,
|
||||
|
||||
// we are showing the login view
|
||||
LOGIN = 2,
|
||||
LOGIN,
|
||||
|
||||
// we are showing the registration view
|
||||
REGISTER = 3,
|
||||
|
||||
// completing the registration flow
|
||||
POST_REGISTRATION = 4,
|
||||
REGISTER,
|
||||
|
||||
// showing the 'forgot password' view
|
||||
FORGOT_PASSWORD = 5,
|
||||
FORGOT_PASSWORD,
|
||||
|
||||
// showing flow to trust this new device with cross-signing
|
||||
COMPLETE_SECURITY = 6,
|
||||
COMPLETE_SECURITY,
|
||||
|
||||
// flow to setup SSSS / cross-signing on this account
|
||||
E2E_SETUP = 7,
|
||||
E2E_SETUP,
|
||||
|
||||
// we are logged in with an active matrix client.
|
||||
LOGGED_IN = 8,
|
||||
// we are logged in with an active matrix client. The logged_in state also
|
||||
// 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
|
||||
// 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
|
||||
// re-dispatched. NOTE: some actions are non-trivial and would require
|
||||
// re-factoring to be included in this list in future.
|
||||
|
@ -199,6 +200,7 @@ interface IState {
|
|||
roomOobData?: object;
|
||||
viaServers?: string[];
|
||||
pendingInitialSync?: boolean;
|
||||
justRegistered?: boolean;
|
||||
}
|
||||
|
||||
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")) {
|
||||
Analytics.enable();
|
||||
}
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
const durationMs = this.stopPageChangeTimer();
|
||||
Analytics.trackPageChange(durationMs);
|
||||
CountlyAnalytics.instance.trackPageChange(durationMs);
|
||||
}
|
||||
if (this.focusComposer) {
|
||||
dis.fire(Action.FocusComposer);
|
||||
|
@ -415,6 +419,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else {
|
||||
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
|
||||
|
@ -473,6 +479,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
const newState = {
|
||||
currentUserId: null,
|
||||
justRegistered: false,
|
||||
};
|
||||
Object.assign(newState, state);
|
||||
this.setState(newState);
|
||||
|
@ -554,11 +561,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
ThemeController.isLogin = true;
|
||||
this.themeWatcher.recheck();
|
||||
break;
|
||||
case 'start_post_registration':
|
||||
this.setState({
|
||||
view: Views.POST_REGISTRATION,
|
||||
});
|
||||
break;
|
||||
case 'start_password_recovery':
|
||||
this.setStateForNewView({
|
||||
view: Views.FORGOT_PASSWORD,
|
||||
|
@ -645,8 +647,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
case Action.ViewRoomDirectory: {
|
||||
const RoomDirectory = sdk.getComponent("structures.RoomDirectory");
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {},
|
||||
'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
Modal.createTrackedDialog('Room directory', '', RoomDirectory, {
|
||||
initialText: payload.initialText,
|
||||
}, 'mx_RoomDirectory_dialogWrapper', false, true);
|
||||
|
||||
// View the welcome or home page if we need something to look at
|
||||
this.viewSomethingBehindModal();
|
||||
|
@ -663,13 +666,13 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.viewWelcome();
|
||||
break;
|
||||
case 'view_home_page':
|
||||
this.viewHome();
|
||||
this.viewHome(payload.justRegistered);
|
||||
break;
|
||||
case 'view_start_chat_or_reuse':
|
||||
this.chatCreateOrReuse(payload.user_id);
|
||||
break;
|
||||
case 'view_create_chat':
|
||||
showStartChatInviteDialog();
|
||||
showStartChatInviteDialog(payload.initialText || "");
|
||||
break;
|
||||
case 'view_invite':
|
||||
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("showCookieBar", null, SettingLevel.DEVICE, false);
|
||||
hideAnalyticsToast();
|
||||
Analytics.enable();
|
||||
if (Analytics.canEnable()) {
|
||||
Analytics.enable();
|
||||
}
|
||||
if (CountlyAnalytics.instance.canEnable()) {
|
||||
CountlyAnalytics.instance.enable(/* anonymous = */ false);
|
||||
}
|
||||
break;
|
||||
case 'reject_cookies':
|
||||
SettingsStore.setValue("analyticsOptIn", null, SettingLevel.DEVICE, false);
|
||||
|
@ -942,10 +950,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.themeWatcher.recheck();
|
||||
}
|
||||
|
||||
private viewHome() {
|
||||
private viewHome(justRegistered = false) {
|
||||
// The home page requires the "logged in" view, so we'll set that.
|
||||
this.setStateForNewView({
|
||||
view: Views.LOGGED_IN,
|
||||
justRegistered,
|
||||
});
|
||||
this.setPage(PageTypes.HomePage);
|
||||
this.notifyNewScreen('home');
|
||||
|
@ -1179,7 +1188,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
if (welcomeUserRoom === null) {
|
||||
// We didn't redirect to the welcome user room, so show
|
||||
// the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
dis.dispatch({action: 'view_home_page', justRegistered: true});
|
||||
}
|
||||
} else if (ThreepidInviteStore.instance.pickBestInvite()) {
|
||||
// The user has a 3pid invite pending - show them that
|
||||
|
@ -1192,7 +1201,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
} else {
|
||||
// The user has just logged in after registering,
|
||||
// so show the homepage.
|
||||
dis.dispatch({action: 'view_home_page'});
|
||||
dis.dispatch({action: 'view_home_page', justRegistered: true});
|
||||
}
|
||||
} else {
|
||||
this.showScreenAfterLogin();
|
||||
|
@ -1200,7 +1209,11 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -1330,8 +1343,8 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
this.firstSyncComplete = true;
|
||||
this.firstSyncPromise.resolve();
|
||||
|
||||
if (Notifier.shouldShowPrompt()) {
|
||||
showNotificationsToast();
|
||||
if (Notifier.shouldShowPrompt() && !MatrixClientPeg.userRegisteredWithinLastHours(24)) {
|
||||
showNotificationsToast(false);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (Lifecycle.isLoggingOut()) return;
|
||||
|
||||
|
@ -1394,6 +1395,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
|
||||
const dft = new DecryptionFailureTracker((total, errorCode) => {
|
||||
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total);
|
||||
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
|
||||
}, (errorCode) => {
|
||||
// Map JS-SDK error codes to tracker codes for aggregation
|
||||
switch (errorCode) {
|
||||
|
@ -1535,6 +1537,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
}
|
||||
|
||||
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') {
|
||||
dis.dispatch({
|
||||
action: 'start_registration',
|
||||
|
@ -1551,7 +1561,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
params: params,
|
||||
});
|
||||
} else if (screen === 'soft_logout') {
|
||||
if (MatrixClientPeg.get() && MatrixClientPeg.get().getUserId() && !Lifecycle.isSoftLogout()) {
|
||||
if (cli.getUserId() && !Lifecycle.isSoftLogout()) {
|
||||
// Logged in - visit a room
|
||||
this.viewLastRoom();
|
||||
} else {
|
||||
|
@ -1581,6 +1591,9 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
action: 'require_registration',
|
||||
});
|
||||
} else if (screen === 'directory') {
|
||||
if (this.state.view === Views.WELCOME) {
|
||||
CountlyAnalytics.instance.track("onboarding_room_directory");
|
||||
}
|
||||
dis.fire(Action.ViewRoomDirectory);
|
||||
} else if (screen === "start_sso" || screen === "start_cas") {
|
||||
// TODO if logged in, skip SSO
|
||||
|
@ -1599,14 +1612,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
dis.dispatch({
|
||||
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) {
|
||||
// Rooms can have the following formats:
|
||||
// #room_alias:domain or !opaque_id:domain
|
||||
|
@ -1777,14 +1782,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
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) {
|
||||
const cli = MatrixClientPeg.get();
|
||||
if (!cli) {
|
||||
|
@ -1949,13 +1946,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
|||
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) {
|
||||
// store errors stop the client syncing and require user intervention, so we'll
|
||||
// be showing a dialog. Don't show anything else.
|
||||
|
|
|
@ -30,6 +30,8 @@ import {_t} from "../../languageHandler";
|
|||
import {haveTileForEvent} from "../views/rooms/EventTile";
|
||||
import {textForEvent} from "../../TextForEvent";
|
||||
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 continuedTypes = ['m.sticker', 'm.room.message'];
|
||||
|
@ -952,15 +954,25 @@ class CreationGrouper {
|
|||
}).reduce((a, b) => a.concat(b), []);
|
||||
// Get sender profile from the latest event in the summary as the m.room.create doesn't contain one
|
||||
const ev = this.events[this.events.length - 1];
|
||||
|
||||
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(
|
||||
<EventListSummary
|
||||
key="roomcreationsummary"
|
||||
events={this.events}
|
||||
onToggle={panel._onHeightChanged} // Update scroll state
|
||||
summaryMembers={[ev.sender]}
|
||||
summaryText={_t("%(creator)s created and configured the room.", {
|
||||
creator: ev.sender ? ev.sender.name : ev.getSender(),
|
||||
})}
|
||||
summaryText={summaryText}
|
||||
>
|
||||
{ eventTiles }
|
||||
</EventListSummary>,
|
||||
|
|
|
@ -33,6 +33,7 @@ import SettingsStore from "../../settings/SettingsStore";
|
|||
import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
|
||||
import GroupStore from "../../stores/GroupStore";
|
||||
import FlairStore from "../../stores/FlairStore";
|
||||
import CountlyAnalytics from "../../CountlyAnalytics";
|
||||
|
||||
const MAX_NAME_LENGTH = 80;
|
||||
const MAX_TOPIC_LENGTH = 800;
|
||||
|
@ -43,12 +44,16 @@ function track(action) {
|
|||
|
||||
export default class RoomDirectory extends React.Component {
|
||||
static propTypes = {
|
||||
initialText: PropTypes.string,
|
||||
onFinished: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
CountlyAnalytics.instance.trackRoomDirectoryBegin();
|
||||
this.startTime = CountlyAnalytics.getTimestamp();
|
||||
|
||||
const selectedCommunityId = GroupFilterOrderStore.getSelectedTags()[0];
|
||||
this.state = {
|
||||
publicRooms: [],
|
||||
|
@ -57,7 +62,7 @@ export default class RoomDirectory extends React.Component {
|
|||
error: null,
|
||||
instanceId: undefined,
|
||||
roomServer: MatrixClientPeg.getHomeserverName(),
|
||||
filterString: null,
|
||||
filterString: this.props.initialText || "",
|
||||
selectedCommunityId: SettingsStore.getValue("feature_communities_v2_prototypes")
|
||||
? selectedCommunityId
|
||||
: null,
|
||||
|
@ -198,6 +203,11 @@ export default class RoomDirectory extends React.Component {
|
|||
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.setState((s) => {
|
||||
s.publicRooms.push(...(data.chunk || []));
|
||||
|
@ -407,7 +417,7 @@ export default class RoomDirectory extends React.Component {
|
|||
};
|
||||
|
||||
onCreateRoomClick = room => {
|
||||
this.props.onFinished();
|
||||
this.onFinished();
|
||||
dis.dispatch({
|
||||
action: 'view_create_room',
|
||||
public: true,
|
||||
|
@ -419,11 +429,12 @@ export default class RoomDirectory extends React.Component {
|
|||
}
|
||||
|
||||
showRoom(room, room_alias, autoJoin = false, shouldPeek = false) {
|
||||
this.props.onFinished();
|
||||
this.onFinished();
|
||||
const payload = {
|
||||
action: 'view_room',
|
||||
auto_join: autoJoin,
|
||||
should_peek: shouldPeek,
|
||||
_type: "room_directory", // instrumentation
|
||||
};
|
||||
if (room) {
|
||||
// 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() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||
|
@ -671,6 +687,7 @@ export default class RoomDirectory extends React.Component {
|
|||
onJoinClick={this.onJoinFromSearchClick}
|
||||
placeholder={placeholder}
|
||||
showJoinButton={showJoinButton}
|
||||
initialText={this.props.initialText}
|
||||
/>
|
||||
{dropdown}
|
||||
</div>;
|
||||
|
@ -693,7 +710,7 @@ export default class RoomDirectory extends React.Component {
|
|||
<BaseDialog
|
||||
className={'mx_RoomDirectory_dialog'}
|
||||
hasCancel={true}
|
||||
onFinished={this.props.onFinished}
|
||||
onFinished={this.onFinished}
|
||||
title={title}
|
||||
>
|
||||
<div className="mx_RoomDirectory">
|
||||
|
|
|
@ -148,7 +148,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
onBlur={this.onBlur}
|
||||
onChange={this.onChange}
|
||||
onKeyDown={this.onKeyDown}
|
||||
placeholder={_t("Search")}
|
||||
placeholder={_t("Filter")}
|
||||
autoComplete="off"
|
||||
/>
|
||||
);
|
||||
|
@ -164,7 +164,7 @@ export default class RoomSearch extends React.PureComponent<IProps, IState> {
|
|||
if (this.props.isMinimized) {
|
||||
icon = (
|
||||
<AccessibleButton
|
||||
title={_t("Search rooms")}
|
||||
title={_t("Filter rooms and people")}
|
||||
className="mx_RoomSearch_icon mx_RoomSearch_minimizedHandle"
|
||||
onClick={this.openSearch}
|
||||
/>
|
||||
|
|
|
@ -18,13 +18,11 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import { _t, _td } from '../../languageHandler';
|
||||
import * as sdk from '../../index';
|
||||
import {MatrixClientPeg} from '../../MatrixClientPeg';
|
||||
import Resend from '../../Resend';
|
||||
import dis from '../../dispatcher/dispatcher';
|
||||
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import { CallState, CallType } from 'matrix-js-sdk/lib/webrtc/call';
|
||||
|
||||
const STATUS_BAR_HIDDEN = 0;
|
||||
const STATUS_BAR_EXPANDED = 1;
|
||||
|
@ -41,16 +39,6 @@ export default class RoomStatusBar extends React.Component {
|
|||
static propTypes = {
|
||||
// the room this statusbar is representing.
|
||||
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
|
||||
// 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
|
||||
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
|
||||
// status bar. This is used to trigger a re-layout in the parent
|
||||
// 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 = () => {
|
||||
Resend.resendUnsentEvents(this.props.room);
|
||||
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
|
||||
// indicate other sizes.
|
||||
_getSize() {
|
||||
if (this._shouldShowConnectionError() ||
|
||||
this._showCallBar() ||
|
||||
this.props.sentMessageAndIsAlone
|
||||
) {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
return STATUS_BAR_EXPANDED;
|
||||
} else if (this.state.unsentMessages.length > 0) {
|
||||
return STATUS_BAR_EXPANDED_LARGE;
|
||||
|
@ -170,22 +145,6 @@ export default class RoomStatusBar extends React.Component {
|
|||
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() {
|
||||
// no conn bar trumps the "some not sent" msg since you can't resend without
|
||||
// a connection!
|
||||
|
@ -276,25 +235,6 @@ export default class RoomStatusBar extends React.Component {
|
|||
</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.
|
||||
_getContent() {
|
||||
if (this._shouldShowConnectionError()) {
|
||||
|
@ -317,44 +257,14 @@ export default class RoomStatusBar extends React.Component {
|
|||
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;
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = this._getContent();
|
||||
const indicator = this._getIndicator();
|
||||
|
||||
return (
|
||||
<div className="mx_RoomStatusBar">
|
||||
<div className="mx_RoomStatusBar_indicator">
|
||||
{ indicator }
|
||||
</div>
|
||||
<div role="alert">
|
||||
{ content }
|
||||
</div>
|
||||
|
|
|
@ -41,7 +41,7 @@ import rateLimitedFunc from '../../ratelimitedfunc';
|
|||
import * as ObjectUtils from '../../ObjectUtils';
|
||||
import * as Rooms from '../../Rooms';
|
||||
import eventSearch, {searchPagination} from '../../Searching';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard';
|
||||
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
|
||||
import MainSplit from './MainSplit';
|
||||
import RightPanel from './RightPanel';
|
||||
import RoomViewStore from '../../stores/RoomViewStore';
|
||||
|
@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
|
|||
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
|
||||
import {Action} from "../../dispatcher/actions";
|
||||
import {SettingLevel} from "../../settings/SettingLevel";
|
||||
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
|
||||
import {IMatrixClientCreds} from "../../MatrixClientPeg";
|
||||
import ScrollPanel from "./ScrollPanel";
|
||||
import TimelinePanel from "./TimelinePanel";
|
||||
|
@ -68,15 +67,16 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
|
|||
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
|
||||
import AuxPanel from "../views/rooms/AuxPanel";
|
||||
import RoomHeader from "../views/rooms/RoomHeader";
|
||||
import TintableSvg from "../views/elements/TintableSvg";
|
||||
import {XOR} from "../../@types/common";
|
||||
import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
|
||||
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/lib/webrtc/call";
|
||||
import EffectsOverlay from "../views/elements/effects/EffectsOverlay";
|
||||
import {containsEmoji} from '../views/elements/effects/effectUtilities';
|
||||
import effects from '../views/elements/effects'
|
||||
import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
|
||||
import WidgetStore from "../../stores/WidgetStore";
|
||||
import {UPDATE_EVENT} from "../../stores/AsyncStore";
|
||||
import Notifier from "../../Notifier";
|
||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||
|
||||
const DEBUG = false;
|
||||
let debuglog = function(msg: string) {};
|
||||
|
@ -132,6 +132,7 @@ export interface IState {
|
|||
initialEventPixelOffset?: number;
|
||||
// Whether to highlight the event scrolled to
|
||||
isInitialEventHighlighted?: boolean;
|
||||
replyToEvent?: MatrixEvent;
|
||||
forwardingEvent?: MatrixEvent;
|
||||
numUnreadMessages: number;
|
||||
draggingFile: boolean;
|
||||
|
@ -150,7 +151,6 @@ export interface IState {
|
|||
guestsCanJoin: boolean;
|
||||
canPeek: boolean;
|
||||
showApps: boolean;
|
||||
isAlone: boolean;
|
||||
isPeeking: boolean;
|
||||
showingPinned: boolean;
|
||||
showReadReceipts: boolean;
|
||||
|
@ -223,7 +223,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
guestsCanJoin: false,
|
||||
canPeek: false,
|
||||
showApps: false,
|
||||
isAlone: false,
|
||||
isPeeking: false,
|
||||
showingPinned: false,
|
||||
showReadReceipts: true,
|
||||
|
@ -320,6 +319,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
joining: RoomViewStore.isJoining(),
|
||||
initialEventId: RoomViewStore.getInitialEventId(),
|
||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
||||
replyToEvent: RoomViewStore.getQuotingEvent(),
|
||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
||||
// we should only peek once we have a ready client
|
||||
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.onResize();
|
||||
|
||||
document.addEventListener("keydown", this.onNativeKeyDown);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
|
@ -597,8 +595,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
|
||||
}
|
||||
|
||||
document.removeEventListener("keydown", this.onNativeKeyDown);
|
||||
|
||||
// Remove RoomStore listener
|
||||
if (this.roomStoreToken) {
|
||||
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 => {
|
||||
let handled = false;
|
||||
|
||||
|
@ -708,9 +677,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
private onAction = payload => {
|
||||
switch (payload.action) {
|
||||
case 'message_send_failed':
|
||||
case 'message_sent':
|
||||
this.checkIfAlone(this.state.room);
|
||||
this.checkDesktopNotifications();
|
||||
break;
|
||||
case 'post_sticker_message':
|
||||
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.
|
||||
private updateRoomMembers = rateLimitedFunc((dueToMember) => {
|
||||
private updateRoomMembers = rateLimitedFunc(() => {
|
||||
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);
|
||||
}, 500);
|
||||
|
||||
private checkIfAlone(room: Room, countInfluence?: number) {
|
||||
let warnedAboutLonelyRoom = false;
|
||||
if (localStorage) {
|
||||
warnedAboutLonelyRoom = Boolean(localStorage.getItem('mx_user_alone_warned_' + this.state.room.roomId));
|
||||
private checkDesktopNotifications() {
|
||||
const memberCount = this.state.room.getJoinedMemberCount() + this.state.room.getInvitedMemberCount();
|
||||
// if they are not alone prompt the user about notifications so they don't miss replies
|
||||
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() {
|
||||
|
@ -1110,14 +1062,6 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
action: 'view_invite',
|
||||
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 = () => {
|
||||
|
@ -1139,6 +1083,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
dis.dispatch({
|
||||
action: 'join_room',
|
||||
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
|
||||
_type: "unknown", // TODO: instrumentation
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
@ -1165,16 +1110,9 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
|
||||
ev.dataTransfer.dropEffect = 'none';
|
||||
|
||||
const items = [...ev.dataTransfer.items];
|
||||
if (items.length >= 1) {
|
||||
const isDraggingFiles = items.every(function(item) {
|
||||
return item.kind == 'file';
|
||||
});
|
||||
|
||||
if (isDraggingFiles) {
|
||||
this.setState({ draggingFile: true });
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
|
||||
this.setState({ draggingFile: true });
|
||||
ev.dataTransfer.dropEffect = 'copy';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1381,10 +1319,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
};
|
||||
|
||||
private onSettingsClick = () => {
|
||||
dis.dispatch({
|
||||
action: Action.SetRightPanelPhase,
|
||||
phase: RightPanelPhases.RoomSummary,
|
||||
});
|
||||
dis.dispatch({ action: "open_room_settings" });
|
||||
};
|
||||
|
||||
private onCancelClick = () => {
|
||||
|
@ -1815,12 +1750,8 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
isStatusAreaExpanded = this.state.statusBarVisible;
|
||||
statusBar = <RoomStatusBar
|
||||
room={this.state.room}
|
||||
sentMessageAndIsAlone={this.state.isAlone}
|
||||
callState={this.state.callState}
|
||||
callType={activeCall ? activeCall.type : null}
|
||||
isPeeking={myMembership !== "join"}
|
||||
onInviteClick={this.onInviteButtonClick}
|
||||
onStopWarningClick={this.onStopAloneWarningClick}
|
||||
onVisible={this.onStatusBarVisible}
|
||||
onHidden={this.onStatusBarHidden}
|
||||
/>;
|
||||
|
@ -1927,6 +1858,7 @@ export default class RoomView extends React.Component<IProps, IState> {
|
|||
showApps={this.state.showApps}
|
||||
e2eStatus={this.state.e2eStatus}
|
||||
resizeNotifier={this.props.resizeNotifier}
|
||||
replyToEvent={this.state.replyToEvent}
|
||||
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
|
||||
// scroll state), but hide it.
|
||||
let searchResultsPanel;
|
||||
|
|
|
@ -704,7 +704,7 @@ export default class ScrollPanel extends React.Component {
|
|||
if (itemlist.style.height !== newHeight) {
|
||||
itemlist.style.height = newHeight;
|
||||
}
|
||||
if (sn.scrollTop !== sn.scrollHeight){
|
||||
if (sn.scrollTop !== sn.scrollHeight) {
|
||||
sn.scrollTop = sn.scrollHeight;
|
||||
}
|
||||
debuglog("updateHeight to", newHeight);
|
||||
|
|
|
@ -55,11 +55,11 @@ export default class ToastContainer extends React.Component<{}, IState> {
|
|||
let toast;
|
||||
if (totalCount !== 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", {
|
||||
"mx_Toast_hasIcon": icon,
|
||||
[`mx_Toast_icon_${icon}`]: icon,
|
||||
});
|
||||
}, className);
|
||||
|
||||
let countIndicator;
|
||||
if (isStacked || this.state.countSeen > 0) {
|
||||
|
|
|
@ -86,7 +86,9 @@ export default class UploadBar extends React.Component {
|
|||
}
|
||||
|
||||
// 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 (
|
||||
<div className="mx_UploadBar">
|
||||
|
|
|
@ -23,7 +23,7 @@ import { _t } from "../../languageHandler";
|
|||
import { ContextMenuButton } from "./ContextMenu";
|
||||
import {USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB} from "../views/dialogs/UserSettingsDialog";
|
||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||
import RedesignFeedbackDialog from "../views/dialogs/RedesignFeedbackDialog";
|
||||
import FeedbackDialog from "../views/dialogs/FeedbackDialog";
|
||||
import Modal from "../../Modal";
|
||||
import LogoutDialog from "../views/dialogs/LogoutDialog";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
|
@ -186,15 +186,22 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
Modal.createTrackedDialog('Report bugs & give feedback', '', RedesignFeedbackDialog);
|
||||
Modal.createTrackedDialog('Feedback Dialog', '', FeedbackDialog);
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onSignOutClick = (ev: ButtonEvent) => {
|
||||
private onSignOutClick = async (ev: ButtonEvent) => {
|
||||
ev.preventDefault();
|
||||
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
|
||||
};
|
||||
|
||||
|
@ -203,6 +210,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
ev.stopPropagation();
|
||||
|
||||
defaultDispatcher.dispatch({action: 'view_home_page'});
|
||||
this.setState({contextMenuPosition: null}); // also close the menu
|
||||
};
|
||||
|
||||
private onCommunitySettingsClick = (ev: ButtonEvent) => {
|
||||
|
@ -452,7 +460,8 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
public render() {
|
||||
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 prototypeCommunityName = CommunityPrototypeStore.instance.getSelectedCommunityName();
|
||||
|
@ -507,7 +516,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
|||
<div className="mx_UserMenu_row">
|
||||
<span className="mx_UserMenu_userAvatarContainer">
|
||||
<BaseAvatar
|
||||
idName={displayName}
|
||||
idName={userId}
|
||||
name={displayName}
|
||||
url={avatarUrl}
|
||||
width={avatarSize}
|
||||
|
|
|
@ -26,6 +26,7 @@ import PasswordReset from "../../../PasswordReset";
|
|||
import AutoDiscoveryUtils, {ValidatedServerConfig} from "../../../utils/AutoDiscoveryUtils";
|
||||
import classNames from 'classnames';
|
||||
import AuthPage from "../../views/auth/AuthPage";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
|
@ -64,6 +65,12 @@ export default class ForgotPassword extends React.Component {
|
|||
serverRequiresIdServer: null,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_forgot_password_begin");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.reset = null;
|
||||
this._checkServerLiveliness(this.props.serverConfig);
|
||||
|
@ -299,15 +306,19 @@ export default class ForgotPassword extends React.Component {
|
|||
value={this.state.email}
|
||||
onChange={this.onInputChanged.bind(this, "email")}
|
||||
autoFocus
|
||||
onFocus={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_focus")}
|
||||
onBlur={() => CountlyAnalytics.instance.track("onboarding_forgot_password_email_blur")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mx_AuthBody_fieldRow">
|
||||
<Field
|
||||
name="reset_password"
|
||||
type="password"
|
||||
label={_t('Password')}
|
||||
label={_t('New Password')}
|
||||
value={this.state.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
|
||||
name="reset_password_confirm"
|
||||
|
@ -315,6 +326,8 @@ export default class ForgotPassword extends React.Component {
|
|||
label={_t('Confirm')}
|
||||
value={this.state.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>
|
||||
<span>{_t(
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2015, 2016, 2017, 2018, 2019 New Vector Ltd
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -16,8 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {ComponentProps, ReactNode} from 'react';
|
||||
|
||||
import {_t, _td} from '../../../languageHandler';
|
||||
import * as sdk from '../../../index';
|
||||
import Login from '../../../Login';
|
||||
|
@ -30,15 +28,13 @@ import SSOButton from "../../views/elements/SSOButton";
|
|||
import PlatformPeg from '../../../PlatformPeg';
|
||||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import {UIFeature} from "../../../settings/UIFeature";
|
||||
|
||||
// For validating phone numbers without country codes
|
||||
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the appropriate login flow(s) for the server
|
||||
const PHASE_LOGIN = 1;
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
import {IMatrixClientCreds} from "../../../MatrixClientPeg";
|
||||
import ServerConfig from "../../views/auth/ServerConfig";
|
||||
import PasswordLogin from "../../views/auth/PasswordLogin";
|
||||
import SignInToText from "../../views/auth/SignInToText";
|
||||
import InlineSpinner from "../../views/elements/InlineSpinner";
|
||||
import Spinner from "../../views/elements/Spinner";
|
||||
|
||||
// Enable phases for login
|
||||
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("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
|
||||
*/
|
||||
export default class LoginComponent extends React.Component {
|
||||
static propTypes = {
|
||||
// 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: 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,
|
||||
};
|
||||
export default class LoginComponent extends React.Component<IProps, IState> {
|
||||
private unmounted = false;
|
||||
private loginLogic: Login;
|
||||
private readonly stepRendererMap: Record<string, () => ReactNode>;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this._unmounted = false;
|
||||
|
||||
this.state = {
|
||||
busy: false,
|
||||
busyLoggingIn: null,
|
||||
errorText: null,
|
||||
loginIncorrect: false,
|
||||
canTryLogin: true, // can we attempt to log in or are there validation errors?
|
||||
|
||||
// used for preserving form values when changing homeserver
|
||||
canTryLogin: true,
|
||||
username: "",
|
||||
phoneCountry: null,
|
||||
phoneNumber: "",
|
||||
|
||||
// Phase of the overall login dialog.
|
||||
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.
|
||||
phase: Phase.Login,
|
||||
currentFlow: null,
|
||||
serverIsAlive: true,
|
||||
serverErrorIsFatal: false,
|
||||
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
|
||||
// letting you do that login type
|
||||
this._stepRendererMap = {
|
||||
'm.login.password': this._renderPasswordStep,
|
||||
this.stepRendererMap = {
|
||||
'm.login.password': this.renderPasswordStep,
|
||||
|
||||
// CAS and SSO are the same thing, modulo the url we link to
|
||||
'm.login.cas': () => this._renderSsoStep("cas"),
|
||||
'm.login.sso': () => this._renderSsoStep("sso"),
|
||||
'm.login.cas': () => this.renderSsoStep("cas"),
|
||||
'm.login.sso': () => this.renderSsoStep("sso"),
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_login_begin");
|
||||
}
|
||||
|
||||
// TODO: [REACT-WARNING] Replace with appropriate lifecycle event
|
||||
// eslint-disable-next-line camelcase
|
||||
UNSAFE_componentWillMount() {
|
||||
this._initLoginLogic();
|
||||
this.initLoginLogic(this.props.serverConfig);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._unmounted = true;
|
||||
this.unmounted = true;
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
|
||||
onPasswordLogin = async (username, phoneCountry, phoneNumber, password) => {
|
||||
|
@ -191,13 +206,13 @@ export default class LoginComponent extends React.Component {
|
|||
loginIncorrect: false,
|
||||
});
|
||||
|
||||
this._loginLogic.loginViaPassword(
|
||||
this.loginLogic.loginViaPassword(
|
||||
username, phoneCountry, phoneNumber, password,
|
||||
).then((data) => {
|
||||
this.setState({serverIsAlive: true}); // it must be, we logged in.
|
||||
this.props.onLoggedIn(data, password);
|
||||
}, (error) => {
|
||||
if (this._unmounted) {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
let errorText;
|
||||
|
@ -209,21 +224,23 @@ export default class LoginComponent extends React.Component {
|
|||
} else if (error.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
error.data.admin_contact,
|
||||
{
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
},
|
||||
);
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
error.data.limit_type,
|
||||
error.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
error.data.admin_contact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using this service."),
|
||||
},
|
||||
);
|
||||
errorText = (
|
||||
<div>
|
||||
<div>{errorTop}</div>
|
||||
|
@ -250,7 +267,7 @@ export default class LoginComponent extends React.Component {
|
|||
}
|
||||
} else {
|
||||
// other errors, not specific to doing a password login
|
||||
errorText = this._errorTextFromError(error);
|
||||
errorText = this.errorTextFromError(error);
|
||||
}
|
||||
|
||||
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
|
||||
// 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
|
||||
// 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.
|
||||
this.setState({
|
||||
busy: false,
|
||||
|
@ -301,7 +318,7 @@ export default class LoginComponent extends React.Component {
|
|||
message = e.translatedMessage;
|
||||
}
|
||||
|
||||
let errorText = message;
|
||||
let errorText: ReactNode = message;
|
||||
let discoveryState = {};
|
||||
if (AutoDiscoveryUtils.isLivelinessError(e)) {
|
||||
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 => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
@ -349,14 +351,14 @@ export default class LoginComponent extends React.Component {
|
|||
};
|
||||
|
||||
onTryRegisterClick = ev => {
|
||||
const step = this._getCurrentFlowStep();
|
||||
const step = this.getCurrentFlowStep();
|
||||
if (step === 'm.login.sso' || step === 'm.login.cas') {
|
||||
// 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'.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
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);
|
||||
} else {
|
||||
// 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({
|
||||
phase: PHASE_LOGIN,
|
||||
phase: Phase.Login,
|
||||
});
|
||||
};
|
||||
|
||||
onEditServerDetailsClick = ev => {
|
||||
private onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
phase: Phase.ServerDetails,
|
||||
});
|
||||
};
|
||||
|
||||
async _initLoginLogic(hsUrl, isUrl) {
|
||||
hsUrl = hsUrl || this.props.serverConfig.hsUrl;
|
||||
isUrl = isUrl || this.props.serverConfig.isUrl;
|
||||
|
||||
private async initLoginLogic({hsUrl, isUrl}: ValidatedServerConfig) {
|
||||
let isDefaultServer = false;
|
||||
if (this.props.serverConfig.isDefault
|
||||
&& hsUrl === this.props.serverConfig.hsUrl
|
||||
|
@ -394,7 +393,7 @@ export default class LoginComponent extends React.Component {
|
|||
const loginLogic = new Login(hsUrl, isUrl, fallbackHsUrl, {
|
||||
defaultDeviceDisplayName: this.props.defaultDeviceDisplayName,
|
||||
});
|
||||
this._loginLogic = loginLogic;
|
||||
this.loginLogic = loginLogic;
|
||||
|
||||
this.setState({
|
||||
busy: true,
|
||||
|
@ -425,7 +424,7 @@ export default class LoginComponent extends React.Component {
|
|||
if (this.state.serverErrorIsFatal) {
|
||||
// Server is dead: show server details prompt instead
|
||||
this.setState({
|
||||
phase: PHASE_SERVER_DETAILS,
|
||||
phase: Phase.ServerDetails,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -434,7 +433,7 @@ export default class LoginComponent extends React.Component {
|
|||
loginLogic.getFlows().then((flows) => {
|
||||
// look for a flow where we understand all of the steps.
|
||||
for (let i = 0; i < flows.length; i++ ) {
|
||||
if (!this._isSupportedFlow(flows[i])) {
|
||||
if (!this.isSupportedFlow(flows[i])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -443,7 +442,7 @@ export default class LoginComponent extends React.Component {
|
|||
// that for now).
|
||||
loginLogic.chooseFlow(i);
|
||||
this.setState({
|
||||
currentFlow: this._getCurrentFlowStep(),
|
||||
currentFlow: this.getCurrentFlowStep(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -457,7 +456,7 @@ export default class LoginComponent extends React.Component {
|
|||
});
|
||||
}, (err) => {
|
||||
this.setState({
|
||||
errorText: this._errorTextFromError(err),
|
||||
errorText: this.errorTextFromError(err),
|
||||
loginIncorrect: 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
|
||||
// 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);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
_getCurrentFlowStep() {
|
||||
return this._loginLogic ? this._loginLogic.getCurrentFlowStep() : null;
|
||||
private getCurrentFlowStep() {
|
||||
return this.loginLogic ? this.loginLogic.getCurrentFlowStep() : null;
|
||||
}
|
||||
|
||||
_errorTextFromError(err) {
|
||||
private errorTextFromError(err) {
|
||||
let errCode = err.errcode;
|
||||
if (!errCode && err.httpStatus) {
|
||||
errCode = "HTTP " + err.httpStatus;
|
||||
}
|
||||
|
||||
let errorText = _t("Error: Problem communicating with the given homeserver.") +
|
||||
(errCode ? " (" + errCode + ")" : "");
|
||||
let errorText: ReactNode = _t("Error: Problem communicating with the given homeserver.") +
|
||||
(errCode ? " (" + errCode + ")" : "");
|
||||
|
||||
if (err.cors === 'rejected') {
|
||||
if (window.location.protocol === 'https:' &&
|
||||
|
@ -499,29 +498,27 @@ export default class LoginComponent extends React.Component {
|
|||
errorText = <span>
|
||||
{ _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>.", {},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener"
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
{
|
||||
'a': (sub) => {
|
||||
return <a target="_blank" rel="noreferrer noopener"
|
||||
href="https://www.google.com/search?&q=enable%20unsafe%20scripts"
|
||||
>
|
||||
{ sub }
|
||||
</a>;
|
||||
},
|
||||
) }
|
||||
}) }
|
||||
</span>;
|
||||
} else {
|
||||
errorText = <span>
|
||||
{ _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 " +
|
||||
"is not blocking requests.", {},
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>,
|
||||
},
|
||||
) }
|
||||
{
|
||||
'a': (sub) =>
|
||||
<a target="_blank" rel="noreferrer noopener" href={this.props.serverConfig.hsUrl}>
|
||||
{ sub }
|
||||
</a>,
|
||||
}) }
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
@ -529,18 +526,16 @@ export default class LoginComponent extends React.Component {
|
|||
return errorText;
|
||||
}
|
||||
|
||||
renderServerComponent() {
|
||||
const ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
|
||||
private renderServerComponent() {
|
||||
if (SdkConfig.get()['disable_custom_urls']) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.ServerDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const serverDetailsProps = {};
|
||||
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
||||
if (PHASES_ENABLED) {
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
|
@ -555,8 +550,8 @@ export default class LoginComponent extends React.Component {
|
|||
/>;
|
||||
}
|
||||
|
||||
renderLoginComponentForStep() {
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_LOGIN) {
|
||||
private renderLoginComponentForStep() {
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.Login) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -566,7 +561,7 @@ export default class LoginComponent extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
const stepRenderer = this._stepRendererMap[step];
|
||||
const stepRenderer = this.stepRendererMap[step];
|
||||
|
||||
if (stepRenderer) {
|
||||
return stepRenderer();
|
||||
|
@ -575,9 +570,7 @@ export default class LoginComponent extends React.Component {
|
|||
return null;
|
||||
}
|
||||
|
||||
_renderPasswordStep = () => {
|
||||
const PasswordLogin = sdk.getComponent('auth.PasswordLogin');
|
||||
|
||||
private renderPasswordStep = () => {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
|
@ -586,29 +579,25 @@ export default class LoginComponent extends React.Component {
|
|||
|
||||
return (
|
||||
<PasswordLogin
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onError={this.onPasswordLoginError}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
initialUsername={this.state.username}
|
||||
initialPhoneCountry={this.state.phoneCountry}
|
||||
initialPhoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onUsernameBlur={this.onUsernameBlur}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onPhoneNumberBlur={this.onPhoneNumberBlur}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
busy={this.props.isSyncing || this.state.busyLoggingIn}
|
||||
onSubmit={this.onPasswordLogin}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
username={this.state.username}
|
||||
phoneCountry={this.state.phoneCountry}
|
||||
phoneNumber={this.state.phoneNumber}
|
||||
onUsernameChanged={this.onUsernameChanged}
|
||||
onUsernameBlur={this.onUsernameBlur}
|
||||
onPhoneCountryChanged={this.onPhoneCountryChanged}
|
||||
onPhoneNumberChanged={this.onPhoneNumberChanged}
|
||||
onForgotPasswordClick={this.props.onForgotPasswordClick}
|
||||
loginIncorrect={this.state.loginIncorrect}
|
||||
serverConfig={this.props.serverConfig}
|
||||
disableSubmit={this.isBusy()}
|
||||
busy={this.props.isSyncing || this.state.busyLoggingIn}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
_renderSsoStep = loginType => {
|
||||
const SignInToText = sdk.getComponent('views.auth.SignInToText');
|
||||
|
||||
private renderSsoStep = loginType => {
|
||||
let onEditServerDetailsClick = null;
|
||||
// If custom URLs are allowed, wire up the server details edit link.
|
||||
if (PHASES_ENABLED && !SdkConfig.get()['disable_custom_urls']) {
|
||||
|
@ -629,7 +618,7 @@ export default class LoginComponent extends React.Component {
|
|||
|
||||
<SSOButton
|
||||
className="mx_Login_sso_link mx_Login_submit"
|
||||
matrixClient={this._loginLogic.createTemporaryClient()}
|
||||
matrixClient={this.loginLogic.createTemporaryClient()}
|
||||
loginType={loginType}
|
||||
fragmentAfterLogin={this.props.fragmentAfterLogin}
|
||||
/>
|
||||
|
@ -638,12 +627,10 @@ export default class LoginComponent extends React.Component {
|
|||
};
|
||||
|
||||
render() {
|
||||
const Loader = sdk.getComponent("elements.Spinner");
|
||||
const InlineSpinner = sdk.getComponent("elements.InlineSpinner");
|
||||
const AuthHeader = sdk.getComponent("auth.AuthHeader");
|
||||
const AuthBody = sdk.getComponent("auth.AuthBody");
|
||||
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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015, 2016, 2017, 2018, 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.
|
||||
|
@ -18,8 +15,9 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import Matrix from 'matrix-js-sdk';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, {ComponentProps, ReactNode} from 'react';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
||||
import * as sdk from '../../../index';
|
||||
import { _t, _td } from '../../../languageHandler';
|
||||
import SdkConfig from '../../../SdkConfig';
|
||||
|
@ -34,36 +32,96 @@ import Login from "../../../Login";
|
|||
import dis from "../../../dispatcher/dispatcher";
|
||||
|
||||
// Phases
|
||||
// Show controls to configure server details
|
||||
const PHASE_SERVER_DETAILS = 0;
|
||||
// Show the appropriate registration flow(s) for the server
|
||||
const PHASE_REGISTRATION = 1;
|
||||
enum Phase {
|
||||
// Show controls to configure server details
|
||||
ServerDetails = 0,
|
||||
// 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
|
||||
const PHASES_ENABLED = true;
|
||||
|
||||
export default class Registration extends React.Component {
|
||||
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,
|
||||
};
|
||||
|
||||
export default class Registration extends React.Component<IProps, IState> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
@ -71,56 +129,22 @@ export default class Registration extends React.Component {
|
|||
this.state = {
|
||||
busy: false,
|
||||
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: {
|
||||
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),
|
||||
serverType,
|
||||
// Phase of the overall registration dialog.
|
||||
phase: PHASE_REGISTRATION,
|
||||
phase: Phase.Registration,
|
||||
flows: null,
|
||||
// If set, we've registered but are not going to log
|
||||
// the user in to their new account automatically.
|
||||
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,
|
||||
serverErrorIsFatal: false,
|
||||
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() {
|
||||
this._unmounted = false;
|
||||
this._replaceClient();
|
||||
this.replaceClient(this.props.serverConfig);
|
||||
}
|
||||
|
||||
// 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 &&
|
||||
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
|
||||
// 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.
|
||||
this.setState({
|
||||
serverType,
|
||||
phase: this.getDefaultPhaseForServerType(serverType),
|
||||
phase: Registration.getDefaultPhaseForServerType(serverType),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getDefaultPhaseForServerType(type) {
|
||||
private static getDefaultPhaseForServerType(type: IState["serverType"]) {
|
||||
switch (type) {
|
||||
case ServerType.FREE: {
|
||||
// Move directly to the registration phase since the server
|
||||
// details are fixed.
|
||||
return PHASE_REGISTRATION;
|
||||
return Phase.Registration;
|
||||
}
|
||||
case ServerType.PREMIUM:
|
||||
case ServerType.ADVANCED:
|
||||
return PHASE_SERVER_DETAILS;
|
||||
return Phase.ServerDetails;
|
||||
}
|
||||
}
|
||||
|
||||
onServerTypeChange = type => {
|
||||
private onServerTypeChange = (type: IState["serverType"]) => {
|
||||
this.setState({
|
||||
serverType: type,
|
||||
});
|
||||
|
@ -181,11 +205,11 @@ export default class Registration extends React.Component {
|
|||
|
||||
// Reset the phase to default phase for the server type.
|
||||
this.setState({
|
||||
phase: this.getDefaultPhaseForServerType(type),
|
||||
phase: Registration.getDefaultPhaseForServerType(type),
|
||||
});
|
||||
};
|
||||
|
||||
async _replaceClient(serverConfig) {
|
||||
private async replaceClient(serverConfig: ValidatedServerConfig) {
|
||||
this.setState({
|
||||
errorText: 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)
|
||||
busy: true,
|
||||
});
|
||||
if (!serverConfig) serverConfig = this.props.serverConfig;
|
||||
|
||||
// Do a liveliness check on the URLs
|
||||
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
|
||||
// need to.
|
||||
if (!this.state.doingUIAuth) {
|
||||
await this._makeRegisterRequest(null);
|
||||
await this.makeRegisterRequest(null);
|
||||
// This should never succeed since we specified no auth object.
|
||||
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({
|
||||
errorText: "",
|
||||
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(
|
||||
emailAddress,
|
||||
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) {
|
||||
let msg = response.message || response.toString();
|
||||
// can we give a better error message?
|
||||
if (response.errcode === 'M_RESOURCE_LIMIT_EXCEEDED') {
|
||||
const errorTop = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'monthly_active_user': _td(
|
||||
"This homeserver has hit its Monthly Active User limit.",
|
||||
),
|
||||
'': _td(
|
||||
"This homeserver has exceeded one of its resource limits.",
|
||||
),
|
||||
});
|
||||
response.data.admin_contact,
|
||||
{
|
||||
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
|
||||
'': _td("This homeserver has exceeded one of its resource limits."),
|
||||
},
|
||||
);
|
||||
const errorDetail = messageForResourceLimitError(
|
||||
response.data.limit_type,
|
||||
response.data.admin_contact, {
|
||||
'': _td(
|
||||
"Please <a>contact your service administrator</a> to continue using this service.",
|
||||
),
|
||||
});
|
||||
response.data.admin_contact,
|
||||
{
|
||||
'': _td("Please <a>contact your service administrator</a> to continue using this service."),
|
||||
},
|
||||
);
|
||||
msg = <div>
|
||||
<p>{errorTop}</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) {
|
||||
let msisdnAvailable = false;
|
||||
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) {
|
||||
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 = {
|
||||
doingUIAuth: false,
|
||||
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
|
||||
|
@ -372,8 +397,6 @@ export default class Registration extends React.Component {
|
|||
`Found a session for ${sessionOwner} but ${response.userId} has just registered.`,
|
||||
);
|
||||
newState.differentLoggedInUserId = sessionOwner;
|
||||
} else {
|
||||
newState.differentLoggedInUserId = null;
|
||||
}
|
||||
|
||||
if (response.access_token) {
|
||||
|
@ -385,9 +408,7 @@ export default class Registration extends React.Component {
|
|||
accessToken: response.access_token,
|
||||
}, this.state.formVals.password);
|
||||
|
||||
this._setupPushers();
|
||||
// we're still busy until we get unmounted: don't show the registration form again
|
||||
newState.busy = true;
|
||||
this.setupPushers();
|
||||
} else {
|
||||
newState.busy = false;
|
||||
newState.completedNoSignin = true;
|
||||
|
@ -396,7 +417,7 @@ export default class Registration extends React.Component {
|
|||
this.setState(newState);
|
||||
};
|
||||
|
||||
_setupPushers() {
|
||||
private setupPushers() {
|
||||
if (!this.props.brand) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -419,38 +440,38 @@ export default class Registration extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
onLoginClick = ev => {
|
||||
private onLoginClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this.props.onLoginClick();
|
||||
};
|
||||
|
||||
onGoToFormClicked = ev => {
|
||||
private onGoToFormClicked = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
this._replaceClient();
|
||||
this.replaceClient(this.props.serverConfig);
|
||||
this.setState({
|
||||
busy: false,
|
||||
doingUIAuth: false,
|
||||
phase: PHASE_REGISTRATION,
|
||||
phase: Phase.Registration,
|
||||
});
|
||||
};
|
||||
|
||||
onServerDetailsNextPhaseClick = async () => {
|
||||
private onServerDetailsNextPhaseClick = async () => {
|
||||
this.setState({
|
||||
phase: PHASE_REGISTRATION,
|
||||
phase: Phase.Registration,
|
||||
});
|
||||
};
|
||||
|
||||
onEditServerDetailsClick = ev => {
|
||||
private onEditServerDetailsClick = ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
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
|
||||
// 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
|
||||
|
@ -466,13 +487,15 @@ export default class Registration extends React.Component {
|
|||
username: this.state.formVals.username,
|
||||
password: this.state.formVals.password,
|
||||
initial_device_display_name: this.props.defaultDeviceDisplayName,
|
||||
auth: undefined,
|
||||
inhibit_login: undefined,
|
||||
};
|
||||
if (auth) registerParams.auth = auth;
|
||||
if (inhibitLogin !== undefined && inhibitLogin !== null) registerParams.inhibit_login = inhibitLogin;
|
||||
return this.state.matrixClient.registerRequest(registerParams);
|
||||
};
|
||||
|
||||
_getUIAuthInputs() {
|
||||
private getUIAuthInputs() {
|
||||
return {
|
||||
emailAddress: this.state.formVals.email,
|
||||
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
|
||||
// which checks the user hasn't already logged in somewhere else (perhaps we should do
|
||||
// this more generally?)
|
||||
_onLoginClickWithCheck = async ev => {
|
||||
private onLoginClickWithCheck = async ev => {
|
||||
ev.preventDefault();
|
||||
|
||||
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 ServerConfig = sdk.getComponent("auth.ServerConfig");
|
||||
const ModularServerConfig = sdk.getComponent("auth.ModularServerConfig");
|
||||
|
@ -502,11 +525,16 @@ export default class Registration extends React.Component {
|
|||
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,
|
||||
// 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
|
||||
// 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>
|
||||
<ServerTypeSelector
|
||||
selected={this.state.serverType}
|
||||
|
@ -515,7 +543,7 @@ export default class Registration extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
const serverDetailsProps = {};
|
||||
const serverDetailsProps: ComponentProps<typeof ServerConfig> = {};
|
||||
if (PHASES_ENABLED) {
|
||||
serverDetailsProps.onAfterSubmit = this.onServerDetailsNextPhaseClick;
|
||||
serverDetailsProps.submitText = _t("Next");
|
||||
|
@ -554,8 +582,8 @@ export default class Registration extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
|
||||
renderRegisterComponent() {
|
||||
if (PHASES_ENABLED && this.state.phase !== PHASE_REGISTRATION) {
|
||||
private renderRegisterComponent() {
|
||||
if (PHASES_ENABLED && this.state.phase !== Phase.Registration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -566,10 +594,10 @@ export default class Registration extends React.Component {
|
|||
if (this.state.matrixClient && this.state.doingUIAuth) {
|
||||
return <InteractiveAuth
|
||||
matrixClient={this.state.matrixClient}
|
||||
makeRequest={this._makeRegisterRequest}
|
||||
onAuthFinished={this._onUIAuthFinished}
|
||||
inputs={this._getUIAuthInputs()}
|
||||
requestEmailToken={this._requestEmailToken}
|
||||
makeRequest={this.makeRegisterRequest}
|
||||
onAuthFinished={this.onUIAuthFinished}
|
||||
inputs={this.getUIAuthInputs()}
|
||||
requestEmailToken={this.requestEmailToken}
|
||||
sessionId={this.props.sessionId}
|
||||
clientSecret={this.props.clientSecret}
|
||||
emailSid={this.props.idSid}
|
||||
|
@ -582,17 +610,6 @@ export default class Registration extends React.Component {
|
|||
<Spinner />
|
||||
</div>;
|
||||
} 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
|
||||
defaultUsername={this.state.formVals.username}
|
||||
defaultEmail={this.state.formVals.email}
|
||||
|
@ -600,7 +617,6 @@ export default class Registration extends React.Component {
|
|||
defaultPhoneNumber={this.state.formVals.phoneNumber}
|
||||
defaultPassword={this.state.formVals.password}
|
||||
onRegisterClick={this.onFormSubmit}
|
||||
onEditServerDetailsClick={onEditServerDetailsClick}
|
||||
flows={this.state.flows}
|
||||
serverConfig={this.props.serverConfig}
|
||||
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
|
||||
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="#">
|
||||
{ _t('Go back') }
|
||||
</a>;
|
||||
|
@ -658,7 +674,7 @@ export default class Registration extends React.Component {
|
|||
loggedInUserId: this.state.differentLoggedInUserId,
|
||||
},
|
||||
)}</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")}
|
||||
</AccessibleButton></p>
|
||||
</div>;
|
||||
|
@ -667,7 +683,7 @@ export default class Registration extends React.Component {
|
|||
regDoneText = <h3>{_t(
|
||||
"<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>;
|
||||
} else {
|
||||
|
@ -677,7 +693,7 @@ export default class Registration extends React.Component {
|
|||
regDoneText = <h3>{_t(
|
||||
"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>;
|
||||
}
|
||||
|
@ -686,11 +702,48 @@ export default class Registration extends React.Component {
|
|||
{ regDoneText }
|
||||
</div>;
|
||||
} 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>
|
||||
<h2>{ _t('Create your account') }</h2>
|
||||
{ errorText }
|
||||
{ serverDeadSection }
|
||||
{ this.renderServerComponent() }
|
||||
{ this.state.phase !== Phase.ServerDetails && <h3>
|
||||
{yourMatrixAccountText}
|
||||
{editLink}
|
||||
</h3> }
|
||||
{ this.renderRegisterComponent() }
|
||||
{ goBack }
|
||||
{ signIn }
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
import React, {createRef} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _t } from '../../../languageHandler';
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
const DIV_ID = 'mx_recaptcha';
|
||||
|
||||
|
@ -45,6 +46,8 @@ export default class CaptchaForm extends React.Component {
|
|||
this._captchaWidgetId = null;
|
||||
|
||||
this._recaptchaContainer = createRef();
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_begin");
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -99,10 +102,16 @@ export default class CaptchaForm extends React.Component {
|
|||
console.log("Loaded recaptcha script.");
|
||||
try {
|
||||
this._renderRecaptcha(DIV_ID);
|
||||
// clear error if re-rendered
|
||||
this.setState({
|
||||
errorText: null,
|
||||
});
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_loaded");
|
||||
} catch (e) {
|
||||
this.setState({
|
||||
errorText: e.toString(),
|
||||
});
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_error", { error: e.toString() });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -123,7 +123,7 @@ export default class CountryDropdown extends React.Component {
|
|||
const options = displayedCountries.map((country) => {
|
||||
return <div className="mx_CountryDropdown_option" key={country.iso2}>
|
||||
{ this._flagImgForIso2(country.iso2) }
|
||||
{ country.name } (+{ country.prefix })
|
||||
{ _t(country.name) } (+{ country.prefix })
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import { _t } from '../../../languageHandler';
|
|||
import SettingsStore from "../../../settings/SettingsStore";
|
||||
import AccessibleButton from "../elements/AccessibleButton";
|
||||
import Spinner from "../elements/Spinner";
|
||||
import CountlyAnalytics from "../../../CountlyAnalytics";
|
||||
|
||||
/* This file contains a collection of components which are used by the
|
||||
* InteractiveAuth to prompt the user to enter the information needed
|
||||
|
@ -189,6 +190,7 @@ export class RecaptchaAuthEntry extends React.Component {
|
|||
}
|
||||
|
||||
_onCaptchaResponse = response => {
|
||||
CountlyAnalytics.instance.track("onboarding_grecaptcha_submit");
|
||||
this.props.submitAuthDict({
|
||||
type: RecaptchaAuthEntry.LOGIN_TYPE,
|
||||
response: response,
|
||||
|
@ -297,6 +299,8 @@ export class TermsAuthEntry extends React.Component {
|
|||
toggledPolicies: initToggles,
|
||||
policies: pickedPolicies,
|
||||
};
|
||||
|
||||
CountlyAnalytics.instance.track("onboarding_terms_begin");
|
||||
}
|
||||
|
||||
|
||||
|
@ -326,8 +330,12 @@ export class TermsAuthEntry extends React.Component {
|
|||
allChecked = allChecked && checked;
|
||||
}
|
||||
|
||||
if (allChecked) this.props.submitAuthDict({type: TermsAuthEntry.LOGIN_TYPE});
|
||||
else this.setState({errorText: _t("Please review and accept all of the homeserver's policies")});
|
||||
if (allChecked) {
|
||||
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() {
|
||||
|
@ -413,12 +421,12 @@ export class EmailIdentityAuthEntry extends React.Component {
|
|||
return <Spinner />;
|
||||
} else {
|
||||
return (
|
||||
<div>
|
||||
<p>{ _t("An email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <i>{ this.props.inputs.emailAddress }</i> },
|
||||
<div className="mx_InteractiveAuthEntryComponents_emailWrapper">
|
||||
<p>{ _t("A confirmation email has been sent to %(emailAddress)s",
|
||||
{ emailAddress: (sub) => <b>{ this.props.inputs.emailAddress }</b> },
|
||||
) }
|
||||
</p>
|
||||
<p>{ _t("Please check your email to continue registration.") }</p>
|
||||
<p>{ _t("Open the link in the email to continue registration.") }</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|