Merge branch 'develop' into develop
|
@ -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/
|
||||
|
|
195
CHANGELOG.md
|
@ -1,3 +1,198 @@
|
|||
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)
|
||||
|
||||
* [Release] Adjust for new widget messaging APIs
|
||||
[\#5342](https://github.com/matrix-org/matrix-react-sdk/pull/5342)
|
||||
* [Release] Fix Jitsi OpenIDC auth
|
||||
[\#5335](https://github.com/matrix-org/matrix-react-sdk/pull/5335)
|
||||
|
||||
Changes in [3.6.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.6.0) (2020-10-12)
|
||||
===================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.6.0-rc.1...v3.6.0)
|
||||
|
|
|
@ -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.0",
|
||||
"version": "3.8.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.3",
|
||||
"matrix-widget-api": "^0.1.0-beta.8",
|
||||
"minimist": "^1.2.5",
|
||||
"pako": "^1.0.11",
|
||||
"parse5": "^5.1.1",
|
||||
|
|
|
@ -32,9 +32,7 @@ do
|
|||
echo "Upgrading $i to $latestver..."
|
||||
yarn add -E $i@$latestver
|
||||
git add -u
|
||||
# The `-e` flag opens the editor and gives you a chance to check
|
||||
# the upgrade for correctness.
|
||||
git commit -m "Upgrade $i to $latestver" -e
|
||||
git commit -m "Upgrade $i to $latestver"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -208,12 +209,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
|||
border: 0;
|
||||
}
|
||||
|
||||
/* applied to side-panels and messagepanel when in RoomSettings */
|
||||
.mx_fadable {
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
// These are magic constants which are excluded from tinting, to let themes
|
||||
// (which only have CSS, unlike skins) tell the app what their non-tinted
|
||||
// colourscheme is by inspecting the stylesheet DOM.
|
||||
|
@ -329,6 +324,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;
|
||||
}
|
||||
|
@ -354,8 +350,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 {
|
||||
|
|
|
@ -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/_TagPanel.scss";
|
||||
@import "./structures/_ToastContainer.scss";
|
||||
@import "./structures/_UploadBar.scss";
|
||||
@import "./structures/_UserMenu.scss";
|
||||
|
@ -51,11 +52,11 @@
|
|||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||
@import "./views/avatars/_PulsedAvatar.scss";
|
||||
@import "./views/avatars/_WidgetAvatar.scss";
|
||||
@import "./views/context_menus/_IconizedContextMenu.scss";
|
||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||
@import "./views/context_menus/_TagTileContextMenu.scss";
|
||||
@import "./views/context_menus/_WidgetContextMenu.scss";
|
||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||
@import "./views/dialogs/_Analytics.scss";
|
||||
@import "./views/dialogs/_BugReportDialog.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";
|
||||
|
@ -107,11 +110,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 +139,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 +183,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 +227,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,13 +16,8 @@ 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: $tagpanel-bg-color;
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
max-height: 40vh;
|
||||
}
|
||||
|
||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_TagPanel {
|
||||
.mx_GroupFilterPanel {
|
||||
flex: 1;
|
||||
background-color: $tagpanel-bg-color;
|
||||
background-color: $groupFilterPanel-bg-color;
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
|
@ -26,49 +26,49 @@ limitations under the License.
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel_items_selected {
|
||||
.mx_GroupFilterPanel_items_selected {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_divider {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_divider {
|
||||
height: 0px;
|
||||
width: 90%;
|
||||
border: none;
|
||||
border-bottom: 1px solid $tagpanel-divider-color;
|
||||
border-bottom: 1px solid $groupFilterPanel-divider-color;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_scroller {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_scroller {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagPanel_tagTileContainer {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
padding-top: 6px;
|
||||
}
|
||||
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
||||
.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div {
|
||||
margin: 6px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile {
|
||||
.mx_GroupFilterPanel .mx_TagTile {
|
||||
// opacity: 0.5;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_prototype {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile:focus,
|
||||
.mx_TagPanel .mx_TagTile:hover,
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected {
|
||||
.mx_GroupFilterPanel .mx_TagTile:focus,
|
||||
.mx_GroupFilterPanel .mx_TagTile:hover,
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected {
|
||||
// opacity: 1;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype {
|
||||
background-color: $primary-bg-color;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
@ -108,7 +108,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile_plus {
|
||||
.mx_GroupFilterPanel .mx_TagTile_plus {
|
||||
margin-bottom: 12px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
@ -132,7 +132,7 @@ limitations under the License.
|
|||
}
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected::before {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected::before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
background-color: $accent-color;
|
||||
|
@ -142,7 +142,7 @@ limitations under the License.
|
|||
border-radius: 0 3px 3px 0;
|
||||
}
|
||||
|
||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||
.mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||
filter: none;
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,29 +14,30 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
$tagPanelWidth: 56px; // only applies in this file, used for calculations
|
||||
$groupFilterPanelWidth: 56px; // only applies in this file, used for calculations
|
||||
|
||||
.mx_LeftPanel {
|
||||
background-color: $roomlist-bg-color;
|
||||
min-width: 260px;
|
||||
max-width: 50%;
|
||||
|
||||
// Create a row-based flexbox for the TagPanel and the room list
|
||||
// Create a row-based flexbox for the GroupFilterPanel and the room list
|
||||
display: flex;
|
||||
|
||||
.mx_LeftPanel_tagPanelContainer {
|
||||
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||
flex-grow: 0;
|
||||
flex-shrink: 0;
|
||||
flex-basis: $tagPanelWidth;
|
||||
flex-basis: $groupFilterPanelWidth;
|
||||
height: 100%;
|
||||
|
||||
// Create another flexbox so the TagPanel fills the container
|
||||
// Create another flexbox so the GroupFilterPanel fills the container
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
// TagPanel handles its own CSS
|
||||
// GroupFilterPanel handles its own CSS
|
||||
}
|
||||
|
||||
&:not(.mx_LeftPanel_hasTagPanel) {
|
||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -45,7 +46,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
|
|||
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||
.mx_LeftPanel_roomListContainer {
|
||||
width: calc(100% - $tagPanelWidth);
|
||||
width: calc(100% - $groupFilterPanelWidth);
|
||||
background-color: $roomlist-bg-color;
|
||||
|
||||
// Create another flexbox (this time a column) for the room list components
|
||||
|
@ -169,10 +170,10 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
|
|||
min-width: unset;
|
||||
|
||||
// We have to forcefully set the width to override the resizer's style attribute.
|
||||
&.mx_LeftPanel_hasTagPanel {
|
||||
width: calc(68px + $tagPanelWidth) !important;
|
||||
&.mx_LeftPanel_hasGroupFilterPanel {
|
||||
width: calc(68px + $groupFilterPanelWidth) !important;
|
||||
}
|
||||
&:not(.mx_LeftPanel_hasTagPanel) {
|
||||
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||
width: 68px !important;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -79,7 +79,6 @@ limitations under the License.
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_MatrixChat > .mx_LeftPanel2:hover + .mx_ResizeHandle_horizontal,
|
||||
.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover {
|
||||
position: relative;
|
||||
|
||||
|
|
|
@ -153,16 +153,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;
|
||||
|
|
|
@ -230,6 +230,10 @@ limitations under the License.
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.mx_UserMenu_contextMenu_hostingLink {
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_IconizedContextMenu_icon {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ActionPayload } from "../payloads";
|
||||
import { Action } from "../actions";
|
||||
|
||||
export interface AppTileActionPayload extends ActionPayload {
|
||||
action: Action.AppTileDelete | Action.AppTileRevoke;
|
||||
widgetId: string;
|
||||
.mx_WidgetAvatar {
|
||||
border-radius: 4px;
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundaction C.I.C.
|
||||
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,23 +14,29 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_WidgetContextMenu {
|
||||
padding: 6px;
|
||||
.mx_ModalWidgetDialog {
|
||||
.mx_ModalWidgetDialog_warning {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.mx_WidgetContextMenu_option {
|
||||
padding: 3px 6px 3px 6px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
> img {
|
||||
vertical-align: middle;
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_WidgetContextMenu_separator {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom-style: none;
|
||||
border-left-style: none;
|
||||
border-right-style: none;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
border-color: $menu-border-color;
|
||||
.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;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,6 +128,13 @@ limitations under the License.
|
|||
mask-size: 20px;
|
||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||
}
|
||||
|
||||
&.mx_AccessibleButton_disabled {
|
||||
padding-right: 12px;
|
||||
&::after {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -110,28 +110,107 @@ limitations under the License.
|
|||
|
||||
.mx_RoomSummaryCard_appsGroup {
|
||||
.mx_RoomSummaryCard_Button {
|
||||
padding-left: 12px;
|
||||
// this button is special so we have to override some of the original styling
|
||||
// as we will be applying it in its children
|
||||
padding: 0;
|
||||
height: auto;
|
||||
color: $tertiary-fg-color;
|
||||
|
||||
span {
|
||||
color: $primary-fg-color;
|
||||
.mx_RoomSummaryCard_icon_app {
|
||||
padding: 10px 48px 10px 12px; // based on typical mx_RoomSummaryCard_Button padding
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
.mx_BaseAvatar_image {
|
||||
vertical-align: top;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
span {
|
||||
color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: top;
|
||||
margin-right: 12px;
|
||||
border-radius: 4px;
|
||||
.mx_RoomSummaryCard_app_pinToggle,
|
||||
.mx_RoomSummaryCard_app_options {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%; // to give bigger interactive zone
|
||||
width: 24px;
|
||||
padding: 12px 4px;
|
||||
box-sizing: border-box;
|
||||
min-width: 24px; // prevent flexbox crushing
|
||||
|
||||
&:hover {
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
top: 8px; // equal to padding-top of parent
|
||||
left: 0;
|
||||
border-radius: 12px;
|
||||
background-color: rgba(141, 151, 165, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: 16px;
|
||||
background-color: $icon-button-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_app_pinToggle {
|
||||
right: 24px;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_app_options {
|
||||
right: 48px;
|
||||
display: none;
|
||||
|
||||
&::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.mx_RoomSummaryCard_Button_pinned {
|
||||
&::after {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_app_pinToggle::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.mx_RoomSummaryCard_icon_app {
|
||||
padding-right: 72px;
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_app_options {
|
||||
display: unset;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_RoomSummaryCard_icon_app_pinned::after {
|
||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
||||
background-color: $accent-color;
|
||||
transform: unset;
|
||||
&::after {
|
||||
top: 8px; // re-align based on the height change
|
||||
pointer-events: none; // pass through to the real button
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -24,34 +24,35 @@ limitations under the License.
|
|||
border: 0;
|
||||
}
|
||||
|
||||
&.mx_WidgetCard_noEdit {
|
||||
.mx_AccessibleButton_kind_secondary {
|
||||
margin: 0 12px;
|
||||
.mx_BaseCard_header {
|
||||
display: inline-flex;
|
||||
|
||||
&:first-child {
|
||||
// expand the Pin to room primary action
|
||||
flex-grow: 1;
|
||||
}
|
||||
& > h2 {
|
||||
margin-right: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_WidgetCard_optionsButton {
|
||||
position: relative;
|
||||
height: 18px;
|
||||
width: 26px;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
.mx_WidgetCard_optionsButton {
|
||||
position: relative;
|
||||
margin-right: 44px;
|
||||
height: 20px;
|
||||
top: 6px;
|
||||
left: 20px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
background-color: $secondary-fg-color;
|
||||
width: 20px;
|
||||
min-width: 20px; // prevent crushing by the flexbox
|
||||
padding: 0;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
top: 0;
|
||||
left: 4px;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
mask-size: contain;
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
background-color: $secondary-fg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,11 +47,27 @@ $MiniAppTileHeight: 200px;
|
|||
opacity: 0.8;
|
||||
background: $primary-fg-color;
|
||||
}
|
||||
|
||||
.mx_ResizeHandle_horizontal::before {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
|
||||
height: 64px; // to match width of the ones on roomlist
|
||||
width: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
content: '';
|
||||
|
||||
background-color: $primary-fg-color;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_hidden {
|
||||
display: none;
|
||||
.mx_AppsContainer_resizer {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.mx_AppsContainer {
|
||||
|
@ -60,53 +76,71 @@ $MiniAppTileHeight: 200px;
|
|||
align-items: stretch;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
|
||||
.mx_AppTile:first-of-type {
|
||||
border-left-width: 8px;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
.mx_AppTile:last-of-type {
|
||||
border-right-width: 8px;
|
||||
border-radius: 0 10px 10px 0;
|
||||
}
|
||||
|
||||
.mx_ResizeHandle_horizontal {
|
||||
position: relative;
|
||||
|
||||
> div {
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_minimised .mx_AppsContainer {
|
||||
// override the re-resizable inline styles
|
||||
height: inherit !important;
|
||||
min-height: inherit !important;
|
||||
}
|
||||
// TODO this should be 300px but that's too large
|
||||
$MinWidth: 240px;
|
||||
|
||||
.mx_AddWidget_button {
|
||||
order: 2;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin: -3px auto 5px 0;
|
||||
color: $accent-color;
|
||||
font-size: $font-12px;
|
||||
}
|
||||
.mx_AppsDrawer_2apps .mx_AppTile {
|
||||
width: 50%;
|
||||
|
||||
.mx_SetAppURLDialog_input {
|
||||
border-radius: 3px;
|
||||
border: 1px solid $input-border-color;
|
||||
padding: 9px;
|
||||
color: $primary-hairline-color;
|
||||
background-color: $primary-bg-color;
|
||||
font-size: $font-15px;
|
||||
&:nth-child(3) {
|
||||
flex-grow: 1;
|
||||
width: 0 !important;
|
||||
min-width: $MinWidth !important;
|
||||
}
|
||||
}
|
||||
.mx_AppsDrawer_3apps .mx_AppTile {
|
||||
width: 33%;
|
||||
|
||||
&:nth-child(3) {
|
||||
flex-grow: 1;
|
||||
width: 0 !important;
|
||||
min-width: $MinWidth !important;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppTile {
|
||||
width: 50%;
|
||||
border: 5px solid $widget-menu-bar-bg-color;
|
||||
border-radius: 4px;
|
||||
min-width: $MinWidth;
|
||||
border: 8px solid $widget-menu-bar-bg-color;
|
||||
border-left-width: 5px;
|
||||
border-right-width: 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
& + .mx_AppTile {
|
||||
margin-left: 5px;
|
||||
}
|
||||
box-sizing: border-box;
|
||||
background-color: $widget-menu-bar-bg-color;
|
||||
}
|
||||
|
||||
.mx_AppTileFullWidth {
|
||||
width: 100%;
|
||||
width: 100% !important; // to override the inline style set by the resizer
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 5px solid $widget-menu-bar-bg-color;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: $widget-menu-bar-bg-color;
|
||||
}
|
||||
|
||||
.mx_AppTile_mini {
|
||||
|
@ -118,12 +152,6 @@ $MiniAppTileHeight: 200px;
|
|||
height: $MiniAppTileHeight;
|
||||
}
|
||||
|
||||
.mx_AppTile.mx_AppTile_minimised,
|
||||
.mx_AppTileFullWidth.mx_AppTile_minimised,
|
||||
.mx_AppTile_mini.mx_AppTile_minimised {
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.mx_AppTile .mx_AppTile_persistedWrapper,
|
||||
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||
|
@ -143,19 +171,20 @@ $MiniAppTileHeight: 200px;
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_expanded {
|
||||
padding-bottom: 5px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBarTitle {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
line-height: 20px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
.mx_WidgetAvatar {
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBarTitle > :last-child {
|
||||
|
@ -179,37 +208,20 @@ $MiniAppTileHeight: 200px;
|
|||
margin: 0 3px;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_minimise {
|
||||
mask-image: url('$(res)/img/feather-customised/widget/minimise.svg');
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_maximise {
|
||||
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout {
|
||||
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
||||
mask-image: url('$(res)/img/icon_context.svg');
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBarWidgetDelete {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBarWidget:hover {
|
||||
border: 1px solid $primary-fg-color;
|
||||
border-radius: 2px;
|
||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||
}
|
||||
|
||||
.mx_AppTileBody {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background-color: $widget-body-bg-color;
|
||||
}
|
||||
|
||||
.mx_AppTileBody_mini {
|
||||
|
@ -242,75 +254,8 @@ $MiniAppTileHeight: 200px;
|
|||
display: block;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBarWidgetPadding {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mx_AppIconTile {
|
||||
background-color: $lightbox-bg-color;
|
||||
border: 1px solid rgba(0, 0, 0, 0);
|
||||
width: 200px;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2);
|
||||
transition: 0.3s;
|
||||
border-radius: 3px;
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.mx_AppIconTile.mx_AppIconTile_active {
|
||||
color: $accent-color;
|
||||
border-color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_AppIconTile:hover {
|
||||
border: 1px solid $accent-color;
|
||||
box-shadow: 0 0 10px 5px rgba(200, 200, 200, 0.5);
|
||||
}
|
||||
|
||||
.mx_AppIconTile_content {
|
||||
padding: 2px 16px;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_content h4 {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_content p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 5px;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_image {
|
||||
padding: 10px;
|
||||
max-width: 100px;
|
||||
max-height: 100px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.mx_AppIconTile_imageContainer {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
border-radius: 3px 3px 0 0;
|
||||
height: 155px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
form.mx_Custom_Widget_Form div {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_AppPermissionWarning {
|
||||
text-align: center;
|
||||
background-color: $widget-menu-bar-bg-color;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
|
@ -375,6 +320,10 @@ form.mx_Custom_Widget_Form div {
|
|||
font-weight: bold;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
// match bg of border so that the cut corners have the right fill
|
||||
background-color: $widget-body-bg-color !important;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mx_AppLoading .mx_Spinner {
|
||||
|
@ -402,10 +351,6 @@ form.mx_Custom_Widget_Form div {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.mx_AppsDrawer_minimised .mx_AppsContainer_resizerHandle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Avoid apptile iframes capturing mouse event focus when resizing */
|
||||
.mx_AppsDrawer_resizing iframe {
|
||||
pointer-events: none;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -70,7 +70,7 @@ limitations under the License.
|
|||
}
|
||||
|
||||
.mx_MemberInfo_avatar {
|
||||
background: $tagpanel-bg-color;
|
||||
background: $groupFilterPanel-bg-color;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -241,6 +241,13 @@ limitations under the License.
|
|||
width: 26px;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_appsButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/apps.svg');
|
||||
}
|
||||
.mx_RoomHeader_appsButton_highlight::before {
|
||||
background-color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_RoomHeader_searchButton::before {
|
||||
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,10 @@
|
|||
border-bottom: none;
|
||||
}
|
||||
|
||||
.mx_AppTileMenuBar {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
iframe {
|
||||
// Sticker picker depends on the fixed height previously used for all tiles
|
||||
height: 273px;
|
||||
|
|
|
@ -85,6 +85,7 @@ limitations under the License.
|
|||
.mx_AvatarSetting_avatarPlaceholder {
|
||||
display: block;
|
||||
height: 90px;
|
||||
width: inherit;
|
||||
border-radius: 90px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export default function(dest, src) {
|
||||
for (const i in src) {
|
||||
if (src.hasOwnProperty(i)) {
|
||||
dest[i] = src[i];
|
||||
}
|
||||
.mx_AnalyticsToast {
|
||||
.mx_AccessibleButton_kind_danger {
|
||||
background: none;
|
||||
color: $accent-color;
|
||||
}
|
||||
|
||||
.mx_AccessibleButton_kind_primary {
|
||||
background: $accent-color;
|
||||
color: #ffffff;
|
||||
}
|
||||
return dest;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -92,3 +92,10 @@ limitations under the License.
|
|||
background-color: $primary-fg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.mx_CallView_video {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
z-index: 30;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,23 +14,17 @@ 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 {
|
||||
.mx_VideoFeed 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;
|
||||
|
@ -39,11 +33,11 @@ limitations under the License.
|
|||
z-index: 100;
|
||||
}
|
||||
|
||||
.mx_VideoView_localVideoFeed video {
|
||||
.mx_VideoFeed_local video {
|
||||
width: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
|
||||
.mx_VideoFeed_mirror video {
|
||||
transform: scale(-1, 1);
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -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.0 KiB |
|
@ -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 |
|
@ -0,0 +1,6 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="14" y="2" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||
<rect x="14" y="14" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||
<rect x="2" y="14" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||
<rect x="2" y="2" width="8" height="8" rx="2" fill="#0DBD8B"/>
|
||||
</svg>
|
After Width: | Height: | Size: 359 B |
|
@ -1,11 +1,21 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" fill="url(#paint0_linear)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 3V9.5H0.00390625L0.00390625 10.5H2V17H0.00390625L0.00390625 18H2V20H3V18H9.5039V20.0005H10.5039V18H17V20H18V18H20.0039V17H18V10.5H20.0039V9.5H18V3H20.0039V2H18V0L17 0V2H10.5039V0.000488281L9.5039 0.000488281V2H3V0L2 0V2H0.00390625L0.00390625 3H2ZM17 3H10.5039V9.5H17V3ZM17 10.5H10.5039V17H17V10.5ZM9.5039 10.5V17H3V10.5H9.5039ZM9.5039 3V9.5H3V3H9.5039Z" fill="white" fill-opacity="0.3" style="mix-blend-mode:lighten"/>
|
||||
<circle opacity="0.8" cx="10.0039" cy="10" r="7.5" stroke="white"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="10" y1="0" x2="10" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#60A6FF"/>
|
||||
<stop offset="1" stop-color="#418DED"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<rect width="20" height="20" rx="4" fill="url(#paint0_linear)"/>
|
||||
<path d="M2.49609 0V20" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||
<path d="M20 2.5L1.60531e-06 2.5" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||
<path d="M20 10L1.60531e-06 10" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||
<path d="M20 17.5H1.60531e-06" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||
<path d="M10 0.000488281V20.0005" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||
<path d="M17.4961 0V20" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||
<circle opacity="0.8" cx="10" cy="10" r="7.5" stroke="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="10" y1="0" x2="10" y2="20" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#60A6FF"/>
|
||||
<stop offset="1" stop-color="#418DED"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0">
|
||||
<rect width="20" height="20.0005" rx="4" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 900 B After Width: | Height: | Size: 1.3 KiB |
|
@ -1,6 +1,6 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.99461" y="1.00002" width="18" height="18" rx="2" fill="white" stroke="#FF4B55" stroke-width="2"/>
|
||||
<rect x="2.96777" y="2" width="16.9843" height="5" fill="#FF4B55"/>
|
||||
<rect x="4.96533" y="9" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
<rect x="11.9585" y="13.0005" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" rx="4" fill="#FF4B55"/>
|
||||
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||
<rect x="3.96826" y="8.99951" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 442 B After Width: | Height: | Size: 430 B |
|
@ -1,5 +1,5 @@
|
|||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1.49609" y="0.500488" width="19" height="19" rx="3.5" fill="#17191C" stroke="#17191C"/>
|
||||
<path d="M18.9961 10.0005C18.9961 14.4188 15.4144 18.0005 10.9961 18.0005C6.57782 18.0005 2.99609 14.4188 2.99609 10.0005C2.99609 5.58221 6.57782 2.00049 10.9961 2.00049C15.4144 2.00049 18.9961 5.58221 18.9961 10.0005Z" fill="white"/>
|
||||
<path d="M10.9961 6.00049V9.81299L13.4961 11.5005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="1" y="1" width="18" height="18" rx="3" fill="#17191C" stroke="#17191C" stroke-width="2"/>
|
||||
<path d="M18 10C18 14.4183 14.4183 18 10 18C5.58172 18 2 14.4183 2 10C2 5.58172 5.58172 2 10 2C14.4183 2 18 5.58172 18 10Z" fill="white"/>
|
||||
<path d="M10 6V9.8125L12.5 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 569 B After Width: | Height: | Size: 469 B |
|
@ -1,4 +1,4 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="-0.000976562" y="0.000488281" width="20" height="20" rx="4" fill="#FCC639"/>
|
||||
<path d="M1.99902 7.00049H17.999V16.5005C17.999 17.3289 17.3274 18.0005 16.499 18.0005H3.49902C2.6706 18.0005 1.99902 17.3289 1.99902 16.5005V7.00049Z" fill="white"/>
|
||||
<rect width="20" height="20" rx="4" fill="#FCC639"/>
|
||||
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 259 B |
|
@ -0,0 +1,5 @@
|
|||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" rx="4" fill="#5ABFF2"/>
|
||||
<path d="M3 7.875C3 6.83947 3.83947 6 4.875 6H11.1875C12.223 6 13.0625 6.83947 13.0625 7.875V12.875C13.0625 13.9105 12.223 14.75 11.1875 14.75H4.875C3.83947 14.75 3 13.9105 3 12.875V7.875Z" fill="white"/>
|
||||
<path d="M14.375 8.44644L16.1208 7.11039C16.4806 6.83502 17 7.09158 17 7.54468V13.0396C17 13.5199 16.4251 13.7669 16.0767 13.4363L14.375 11.8214V8.44644Z" fill="white"/>
|
||||
</svg>
|
After Width: | Height: | Size: 543 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="M2 6C2 3.79086 3.79086 2 6 2H18C20.2091 2 22 3.79086 22 6V18C22 20.2091 20.2091 22 18 22H6C3.79086 22 2 20.2091 2 18V6ZM11 8C11 9.65685 9.65685 11 8 11C6.34315 11 5 9.65685 5 8C5 6.34315 6.34315 5 8 5C9.65685 5 11 6.34315 11 8ZM8 19C9.65685 19 11 17.6569 11 16C11 14.3431 9.65685 13 8 13C6.34315 13 5 14.3431 5 16C5 17.6569 6.34315 19 8 19ZM19 16C19 17.6569 17.6569 19 16 19C14.3431 19 13 17.6569 13 16C13 14.3431 14.3431 13 16 13C17.6569 13 19 14.3431 19 16ZM16 11C17.6569 11 19 9.65685 19 8C19 6.34315 17.6569 5 16 5C14.3431 5 13 6.34315 13 8C13 9.65685 14.3431 11 16 11Z" fill="black"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 742 B |
|
@ -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 |
|
@ -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 |
|
@ -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,5 +0,0 @@
|
|||
<svg width="3" height="15" viewBox="0 0 3 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 3C2.32843 3 3 2.32843 3 1.5C3 0.671573 2.32843 0 1.5 0C0.671573 0 0 0.671573 0 1.5C0 2.32843 0.671573 3 1.5 3Z" fill="#9FA9BA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 9C2.32843 9 3 8.32843 3 7.5C3 6.67157 2.32843 6 1.5 6C0.671573 6 0 6.67157 0 7.5C0 8.32843 0.671573 9 1.5 9Z" fill="#9FA9BA"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 15C2.32843 15 3 14.3284 3 13.5C3 12.6716 2.32843 12 1.5 12C0.671573 12 0 12.6716 0 13.5C0 14.3284 0.671573 15 1.5 15Z" fill="#9FA9BA"/>
|
||||
</svg>
|
Before Width: | Height: | Size: 655 B |
|
@ -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 |
|
@ -39,7 +39,7 @@ $info-plinth-fg-color: #888;
|
|||
|
||||
$preview-bar-bg-color: $header-panel-bg-color;
|
||||
|
||||
$tagpanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||
$groupFilterPanel-bg-color: rgba(38, 39, 43, 0.82);
|
||||
$inverted-bg-color: $base-color;
|
||||
|
||||
// used by AddressSelector
|
||||
|
@ -98,7 +98,7 @@ $roomheader-color: $text-primary-color;
|
|||
$roomheader-bg-color: $bg-color;
|
||||
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3);
|
||||
$roomheader-addroom-fg-color: $text-primary-color;
|
||||
$tagpanel-button-color: $header-panel-text-primary-color;
|
||||
$groupFilterPanel-button-color: $header-panel-text-primary-color;
|
||||
$groupheader-button-color: $header-panel-text-primary-color;
|
||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||
$icon-button-color: #8E99A4;
|
||||
|
@ -117,8 +117,9 @@ $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%);
|
||||
|
||||
$tagpanel-divider-color: $roomlist-header-color;
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
@ -131,6 +132,7 @@ $notice-secondary-color: $roomlist-header-color;
|
|||
$panel-divider-color: transparent;
|
||||
|
||||
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
||||
$widget-body-bg-color: rgba(141, 151, 165, 0.2);
|
||||
|
||||
// event tile lifecycle
|
||||
$event-sending-color: $text-secondary-color;
|
||||
|
@ -187,7 +189,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
|||
|
||||
$kbd-border-color: #000000;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
|
@ -202,7 +204,7 @@ $appearance-tab-border-color: $room-highlight-color;
|
|||
|
||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||
$roomlist-background-blur-amount: 60px;
|
||||
$tagpanel-background-blur-amount: 30px;
|
||||
$groupFilterPanel-background-blur-amount: 30px;
|
||||
|
||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
@import "../../light/css/_fonts.scss";
|
||||
@import "../../light/css/_light.scss";
|
||||
// important this goes before _mods,
|
||||
// as $tagpanel-background-blur-amount and
|
||||
// as $groupFilterPanel-background-blur-amount and
|
||||
// $roomlist-background-blur-amount
|
||||
// are overridden in _dark.scss
|
||||
@import "_dark.scss";
|
||||
|
|
|
@ -37,8 +37,8 @@ $info-plinth-fg-color: #888;
|
|||
|
||||
$preview-bar-bg-color: $header-panel-bg-color;
|
||||
|
||||
$tagpanel-bg-color: $base-color;
|
||||
$inverted-bg-color: $tagpanel-bg-color;
|
||||
$groupFilterPanel-bg-color: $base-color;
|
||||
$inverted-bg-color: $groupFilterPanel-bg-color;
|
||||
|
||||
// used by AddressSelector
|
||||
$selected-color: $room-highlight-color;
|
||||
|
@ -95,7 +95,7 @@ $topleftmenu-color: $text-primary-color;
|
|||
$roomheader-color: $text-primary-color;
|
||||
$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity
|
||||
$roomheader-addroom-fg-color: $text-primary-color;
|
||||
$tagpanel-button-color: $header-panel-text-primary-color;
|
||||
$groupFilterPanel-button-color: $header-panel-text-primary-color;
|
||||
$groupheader-button-color: $header-panel-text-primary-color;
|
||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||
$icon-button-color: $header-panel-text-primary-color;
|
||||
|
@ -114,8 +114,9 @@ $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%);
|
||||
|
||||
$tagpanel-divider-color: $roomlist-header-color;
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
@ -126,6 +127,7 @@ $roomtile-selected-bg-color: #1A1D23;
|
|||
$panel-divider-color: $header-panel-border-color;
|
||||
|
||||
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
||||
$widget-body-bg-color: #1A1D23;
|
||||
|
||||
// event tile lifecycle
|
||||
$event-sending-color: $text-secondary-color;
|
||||
|
@ -182,7 +184,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
|||
|
||||
$kbd-border-color: #000000;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: $base-color;
|
||||
|
|
|
@ -67,8 +67,8 @@ $preview-bar-bg-color: #f7f7f7;
|
|||
$secondary-accent-color: #f2f5f8;
|
||||
$tertiary-accent-color: #d3efe1;
|
||||
|
||||
$tagpanel-bg-color: #27303a;
|
||||
$inverted-bg-color: $tagpanel-bg-color;
|
||||
$groupFilterPanel-bg-color: #27303a;
|
||||
$inverted-bg-color: $groupFilterPanel-bg-color;
|
||||
|
||||
// used by RoomDirectory permissions
|
||||
$plinth-bg-color: $secondary-accent-color;
|
||||
|
@ -162,7 +162,7 @@ $roomheader-color: #45474a;
|
|||
$roomheader-bg-color: $primary-bg-color;
|
||||
$roomheader-addroom-bg-color: #91a1c0;
|
||||
$roomheader-addroom-fg-color: $accent-fg-color;
|
||||
$tagpanel-button-color: #91a1c0;
|
||||
$groupFilterPanel-button-color: #91a1c0;
|
||||
$groupheader-button-color: #91a1c0;
|
||||
$rightpanel-button-color: #91a1c0;
|
||||
$icon-button-color: #91a1c0;
|
||||
|
@ -181,8 +181,9 @@ $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%);
|
||||
|
||||
$tagpanel-divider-color: $roomlist-header-color;
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: #9e9e9e;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
@ -208,6 +209,7 @@ $panel-divider-color: #dee1f3;
|
|||
// ********************
|
||||
|
||||
$widget-menu-bar-bg-color: $secondary-accent-color;
|
||||
$widget-body-bg-color: #fff;
|
||||
|
||||
// ********************
|
||||
|
||||
|
@ -305,7 +307,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
|||
|
||||
$kbd-border-color: $reaction-row-button-border-color;
|
||||
|
||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
||||
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||
$tooltip-timeline-fg-color: #ffffff;
|
||||
|
||||
$interactive-tooltip-bg-color: #27303a;
|
||||
|
|
|
@ -49,7 +49,7 @@ $roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
|||
//
|
||||
// --sidebar-color
|
||||
$interactive-tooltip-bg-color: var(--sidebar-color);
|
||||
$tagpanel-bg-color: var(--sidebar-color);
|
||||
$groupFilterPanel-bg-color: var(--sidebar-color);
|
||||
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||
$roomlist-button-bg-color: var(--sidebar-color-15pct);
|
||||
|
|
|
@ -62,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7;
|
|||
$secondary-accent-color: #f2f5f8;
|
||||
$tertiary-accent-color: #d3efe1;
|
||||
|
||||
$tagpanel-bg-color: rgba(232, 232, 232, 0.77);
|
||||
$groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
|
||||
|
||||
// used by RoomDirectory permissions
|
||||
$plinth-bg-color: $secondary-accent-color;
|
||||
|
@ -156,7 +156,7 @@ $roomheader-color: #45474a;
|
|||
$roomheader-bg-color: $primary-bg-color;
|
||||
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2);
|
||||
$roomheader-addroom-fg-color: #5c6470;
|
||||
$tagpanel-button-color: #91A1C0;
|
||||
$groupFilterPanel-button-color: #91A1C0;
|
||||
$groupheader-button-color: #91A1C0;
|
||||
$rightpanel-button-color: #91A1C0;
|
||||
$icon-button-color: #C1C6CD;
|
||||
|
@ -175,8 +175,9 @@ $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%);
|
||||
|
||||
$tagpanel-divider-color: $roomlist-header-color;
|
||||
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||
|
||||
$roomtile-preview-color: $secondary-fg-color;
|
||||
$roomtile-default-badge-bg-color: #61708b;
|
||||
|
@ -208,6 +209,7 @@ $pinned-color: $notice-secondary-color;
|
|||
// ********************
|
||||
|
||||
$widget-menu-bar-bg-color: $secondary-accent-color;
|
||||
$widget-body-bg-color: #FFF;
|
||||
|
||||
// ********************
|
||||
|
||||
|
@ -320,7 +322,7 @@ $appearance-tab-border-color: $input-darker-bg-color;
|
|||
|
||||
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||
$roomlist-background-blur-amount: 40px;
|
||||
$tagpanel-background-blur-amount: 20px;
|
||||
$groupFilterPanel-background-blur-amount: 20px;
|
||||
|
||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
|
||||
@supports (backdrop-filter: none) {
|
||||
.mx_LeftPanel {
|
||||
background-image: var(--avatar-url);
|
||||
background-image: var(--avatar-url, unset);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: left top;
|
||||
}
|
||||
|
||||
.mx_TagPanel {
|
||||
backdrop-filter: blur($tagpanel-background-blur-amount);
|
||||
.mx_GroupFilterPanel {
|
||||
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||
}
|
||||
|
||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||
|
|
|
@ -32,6 +32,10 @@ import type {Renderer} from "react-dom";
|
|||
import RightPanelStore from "../stores/RightPanelStore";
|
||||
import WidgetStore from "../stores/WidgetStore";
|
||||
import CallHandler from "../CallHandler";
|
||||
import {Analytics} from "../Analytics";
|
||||
import CountlyAnalytics from "../CountlyAnalytics";
|
||||
import UserActivity from "../UserActivity";
|
||||
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
@ -56,11 +60,22 @@ declare global {
|
|||
mxRightPanelStore: RightPanelStore;
|
||||
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 {
|
||||
|
@ -90,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
||||
import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler';
|
||||
import PlatformPeg from './PlatformPeg';
|
||||
import SdkConfig from './SdkConfig';
|
||||
import Modal from './Modal';
|
||||
|
@ -27,7 +27,7 @@ const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password
|
|||
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
||||
|
||||
// Remove all but the first item in the hash path. Redact unexpected hashes.
|
||||
function getRedactedHash(hash) {
|
||||
function getRedactedHash(hash: string): string {
|
||||
// Don't leak URLs we aren't expecting - they could contain tokens/PII
|
||||
const match = hashRegex.exec(hash);
|
||||
if (!match) {
|
||||
|
@ -44,7 +44,7 @@ function getRedactedHash(hash) {
|
|||
|
||||
// Return the current origin, path and hash separated with a `/`. This does
|
||||
// not include query parameters.
|
||||
function getRedactedUrl() {
|
||||
function getRedactedUrl(): string {
|
||||
const { origin, hash } = window.location;
|
||||
let { pathname } = window.location;
|
||||
|
||||
|
@ -56,7 +56,25 @@ function getRedactedUrl() {
|
|||
return origin + pathname + getRedactedHash(hash);
|
||||
}
|
||||
|
||||
const customVariables = {
|
||||
interface IData {
|
||||
/* eslint-disable camelcase */
|
||||
gt_ms?: string;
|
||||
e_c?: string;
|
||||
e_a?: string;
|
||||
e_n?: string;
|
||||
e_v?: string;
|
||||
ping?: string;
|
||||
/* eslint-enable camelcase */
|
||||
}
|
||||
|
||||
interface IVariable {
|
||||
id: number;
|
||||
expl: string; // explanation
|
||||
example: string; // example value
|
||||
getTextVariables?(): IVariables; // object to pass as 2nd argument to `_t`
|
||||
}
|
||||
|
||||
const customVariables: Record<string, IVariable> = {
|
||||
// The Matomo installation at https://matomo.riot.im is currently configured
|
||||
// with a limit of 10 custom variables.
|
||||
'App Platform': {
|
||||
|
@ -120,7 +138,7 @@ const customVariables = {
|
|||
},
|
||||
};
|
||||
|
||||
function whitelistRedact(whitelist, str) {
|
||||
function whitelistRedact(whitelist: string[], str: string): string {
|
||||
if (whitelist.includes(str)) return str;
|
||||
return '<redacted>';
|
||||
}
|
||||
|
@ -130,7 +148,7 @@ const CREATION_TS_KEY = "mx_Riot_Analytics_cts";
|
|||
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
|
||||
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
|
||||
|
||||
function getUid() {
|
||||
function getUid(): string {
|
||||
try {
|
||||
let data = localStorage && localStorage.getItem(UID_KEY);
|
||||
if (!data && localStorage) {
|
||||
|
@ -145,32 +163,36 @@ function getUid() {
|
|||
|
||||
const HEARTBEAT_INTERVAL = 30 * 1000; // seconds
|
||||
|
||||
class Analytics {
|
||||
export class Analytics {
|
||||
private baseUrl: URL = null;
|
||||
private siteId: string = null;
|
||||
private visitVariables: Record<number, [string, string]> = {}; // {[id: number]: [name: string, value: string]}
|
||||
private firstPage = true;
|
||||
private heartbeatIntervalID: number = null;
|
||||
|
||||
private readonly creationTs: string;
|
||||
private readonly lastVisitTs: string;
|
||||
private readonly visitCount: string;
|
||||
|
||||
constructor() {
|
||||
this.baseUrl = null;
|
||||
this.siteId = null;
|
||||
this.visitVariables = {};
|
||||
|
||||
this.firstPage = true;
|
||||
this._heartbeatIntervalID = null;
|
||||
|
||||
this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY);
|
||||
if (!this.creationTs && localStorage) {
|
||||
localStorage.setItem(CREATION_TS_KEY, this.creationTs = new Date().getTime());
|
||||
localStorage.setItem(CREATION_TS_KEY, this.creationTs = String(new Date().getTime()));
|
||||
}
|
||||
|
||||
this.lastVisitTs = localStorage && localStorage.getItem(LAST_VISIT_TS_KEY);
|
||||
this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || 0;
|
||||
this.visitCount = localStorage && localStorage.getItem(VISIT_COUNT_KEY) || "0";
|
||||
this.visitCount = String(parseInt(this.visitCount, 10) + 1); // increment
|
||||
if (localStorage) {
|
||||
localStorage.setItem(VISIT_COUNT_KEY, parseInt(this.visitCount, 10) + 1);
|
||||
localStorage.setItem(VISIT_COUNT_KEY, this.visitCount);
|
||||
}
|
||||
}
|
||||
|
||||
get disabled() {
|
||||
public get disabled() {
|
||||
return !this.baseUrl;
|
||||
}
|
||||
|
||||
canEnable() {
|
||||
public canEnable() {
|
||||
const config = SdkConfig.get();
|
||||
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
|
||||
}
|
||||
|
@ -179,67 +201,67 @@ class Analytics {
|
|||
* Enable Analytics if initialized but disabled
|
||||
* otherwise try and initalize, no-op if piwik config missing
|
||||
*/
|
||||
async enable() {
|
||||
public async enable() {
|
||||
if (!this.disabled) return;
|
||||
if (!this.canEnable()) return;
|
||||
const config = SdkConfig.get();
|
||||
|
||||
this.baseUrl = new URL("piwik.php", config.piwik.url);
|
||||
// set constants
|
||||
this.baseUrl.searchParams.set("rec", 1); // rec is required for tracking
|
||||
this.baseUrl.searchParams.set("rec", "1"); // rec is required for tracking
|
||||
this.baseUrl.searchParams.set("idsite", config.piwik.siteId); // rec is required for tracking
|
||||
this.baseUrl.searchParams.set("apiv", 1); // API version to use
|
||||
this.baseUrl.searchParams.set("send_image", 0); // we want a 204, not a tiny GIF
|
||||
this.baseUrl.searchParams.set("apiv", "1"); // API version to use
|
||||
this.baseUrl.searchParams.set("send_image", "0"); // we want a 204, not a tiny GIF
|
||||
// set user parameters
|
||||
this.baseUrl.searchParams.set("_id", getUid()); // uuid
|
||||
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts
|
||||
this.baseUrl.searchParams.set("_idvc", parseInt(this.visitCount, 10)+ 1); // visit count
|
||||
this.baseUrl.searchParams.set("_idvc", this.visitCount); // visit count
|
||||
if (this.lastVisitTs) {
|
||||
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
|
||||
}
|
||||
|
||||
const platform = PlatformPeg.get();
|
||||
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||
this.setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||
try {
|
||||
this._setVisitVariable('App Version', await platform.getAppVersion());
|
||||
this.setVisitVariable('App Version', await platform.getAppVersion());
|
||||
} catch (e) {
|
||||
this._setVisitVariable('App Version', 'unknown');
|
||||
this.setVisitVariable('App Version', 'unknown');
|
||||
}
|
||||
|
||||
this._setVisitVariable('Chosen Language', getCurrentLanguage());
|
||||
this.setVisitVariable('Chosen Language', getCurrentLanguage());
|
||||
|
||||
const hostname = window.location.hostname;
|
||||
if (hostname === 'riot.im') {
|
||||
this._setVisitVariable('Instance', window.location.pathname);
|
||||
this.setVisitVariable('Instance', window.location.pathname);
|
||||
} else if (hostname.endsWith('.element.io')) {
|
||||
this._setVisitVariable('Instance', hostname.replace('.element.io', ''));
|
||||
this.setVisitVariable('Instance', hostname.replace('.element.io', ''));
|
||||
}
|
||||
|
||||
let installedPWA = "unknown";
|
||||
try {
|
||||
// Known to work at least for desktop Chrome
|
||||
installedPWA = window.matchMedia('(display-mode: standalone)').matches;
|
||||
installedPWA = String(window.matchMedia('(display-mode: standalone)').matches);
|
||||
} catch (e) { }
|
||||
this._setVisitVariable('Installed PWA', installedPWA);
|
||||
this.setVisitVariable('Installed PWA', installedPWA);
|
||||
|
||||
let touchInput = "unknown";
|
||||
try {
|
||||
// MDN claims broad support across browsers
|
||||
touchInput = window.matchMedia('(pointer: coarse)').matches;
|
||||
touchInput = String(window.matchMedia('(pointer: coarse)').matches);
|
||||
} catch (e) { }
|
||||
this._setVisitVariable('Touch Input', touchInput);
|
||||
this.setVisitVariable('Touch Input', touchInput);
|
||||
|
||||
// start heartbeat
|
||||
this._heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
|
||||
this.heartbeatIntervalID = window.setInterval(this.ping.bind(this), HEARTBEAT_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable Analytics, stop the heartbeat and clear identifiers from localStorage
|
||||
*/
|
||||
disable() {
|
||||
public disable() {
|
||||
if (this.disabled) return;
|
||||
this.trackEvent('Analytics', 'opt-out');
|
||||
window.clearInterval(this._heartbeatIntervalID);
|
||||
window.clearInterval(this.heartbeatIntervalID);
|
||||
this.baseUrl = null;
|
||||
this.visitVariables = {};
|
||||
localStorage.removeItem(UID_KEY);
|
||||
|
@ -248,7 +270,7 @@ class Analytics {
|
|||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||
}
|
||||
|
||||
async _track(data) {
|
||||
private async _track(data: IData) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const now = new Date();
|
||||
|
@ -264,13 +286,13 @@ class Analytics {
|
|||
s: now.getSeconds(),
|
||||
};
|
||||
|
||||
const url = new URL(this.baseUrl);
|
||||
const url = new URL(this.baseUrl.toString()); // copy
|
||||
for (const key in params) {
|
||||
url.searchParams.set(key, params[key]);
|
||||
}
|
||||
|
||||
try {
|
||||
await window.fetch(url, {
|
||||
await window.fetch(url.toString(), {
|
||||
method: "GET",
|
||||
mode: "no-cors",
|
||||
cache: "no-cache",
|
||||
|
@ -281,14 +303,14 @@ class Analytics {
|
|||
}
|
||||
}
|
||||
|
||||
ping() {
|
||||
public ping() {
|
||||
this._track({
|
||||
ping: 1,
|
||||
ping: "1",
|
||||
});
|
||||
localStorage.setItem(LAST_VISIT_TS_KEY, new Date().getTime()); // update last visit ts
|
||||
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
|
||||
}
|
||||
|
||||
trackPageChange(generationTimeMs) {
|
||||
public trackPageChange(generationTimeMs?: number) {
|
||||
if (this.disabled) return;
|
||||
if (this.firstPage) {
|
||||
// De-duplicate first page
|
||||
|
@ -303,11 +325,11 @@ class Analytics {
|
|||
}
|
||||
|
||||
this._track({
|
||||
gt_ms: generationTimeMs,
|
||||
gt_ms: String(generationTimeMs),
|
||||
});
|
||||
}
|
||||
|
||||
trackEvent(category, action, name, value) {
|
||||
public trackEvent(category: string, action: string, name?: string, value?: string) {
|
||||
if (this.disabled) return;
|
||||
this._track({
|
||||
e_c: category,
|
||||
|
@ -317,12 +339,12 @@ class Analytics {
|
|||
});
|
||||
}
|
||||
|
||||
_setVisitVariable(key, value) {
|
||||
private setVisitVariable(key: keyof typeof customVariables, value: string) {
|
||||
if (this.disabled) return;
|
||||
this.visitVariables[customVariables[key].id] = [key, value];
|
||||
}
|
||||
|
||||
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
||||
public setLoggedIn(isGuest: boolean, homeserverUrl: string) {
|
||||
if (this.disabled) return;
|
||||
|
||||
const config = SdkConfig.get();
|
||||
|
@ -330,16 +352,16 @@ class Analytics {
|
|||
|
||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
||||
|
||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||
this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||
this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||
}
|
||||
|
||||
setBreadcrumbs(state) {
|
||||
public setBreadcrumbs(state: boolean) {
|
||||
if (this.disabled) return;
|
||||
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||
this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||
}
|
||||
|
||||
showDetailsModal = () => {
|
||||
public showDetailsModal = () => {
|
||||
let rows = [];
|
||||
if (!this.disabled) {
|
||||
rows = Object.values(this.visitVariables);
|
||||
|
@ -360,7 +382,7 @@ class Analytics {
|
|||
'e.g. <CurrentPageURL>',
|
||||
{},
|
||||
{
|
||||
CurrentPageURL: getRedactedUrl(),
|
||||
CurrentPageURL: getRedactedUrl,
|
||||
},
|
||||
),
|
||||
},
|
||||
|
@ -401,7 +423,7 @@ class Analytics {
|
|||
};
|
||||
}
|
||||
|
||||
if (!global.mxAnalytics) {
|
||||
global.mxAnalytics = new Analytics();
|
||||
if (!window.mxAnalytics) {
|
||||
window.mxAnalytics = new Analytics();
|
||||
}
|
||||
export default global.mxAnalytics;
|
||||
export default window.mxAnalytics;
|
|
@ -14,14 +14,19 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
import {User} from "matrix-js-sdk/src/models/user";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import DMRoomMap from './utils/DMRoomMap';
|
||||
import {getHttpUriForMxc} from "matrix-js-sdk/src/content-repo";
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
|
||||
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||
export function avatarUrlForMember(member, width, height, resizeMethod) {
|
||||
let url;
|
||||
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||
let url: string;
|
||||
if (member && member.getAvatarUrl) {
|
||||
url = member.getAvatarUrl(
|
||||
MatrixClientPeg.get().getHomeserverUrl(),
|
||||
|
@ -41,7 +46,7 @@ export function avatarUrlForMember(member, width, height, resizeMethod) {
|
|||
return url;
|
||||
}
|
||||
|
||||
export function avatarUrlForUser(user, width, height, resizeMethod) {
|
||||
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
const url = getHttpUriForMxc(
|
||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||
Math.floor(width * window.devicePixelRatio),
|
||||
|
@ -54,14 +59,14 @@ export function avatarUrlForUser(user, width, height, resizeMethod) {
|
|||
return url;
|
||||
}
|
||||
|
||||
function isValidHexColor(color) {
|
||||
function isValidHexColor(color: string): boolean {
|
||||
return typeof color === "string" &&
|
||||
(color.length === 7 || color.lengh === 9) &&
|
||||
(color.length === 7 || color.length === 9) &&
|
||||
color.charAt(0) === "#" &&
|
||||
!color.substr(1).split("").some(c => isNaN(parseInt(c, 16)));
|
||||
}
|
||||
|
||||
function urlForColor(color) {
|
||||
function urlForColor(color: string): string {
|
||||
const size = 40;
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = size;
|
||||
|
@ -79,9 +84,9 @@ function urlForColor(color) {
|
|||
// XXX: Ideally we'd clear this cache when the theme changes
|
||||
// but since this function is at global scope, it's a bit
|
||||
// hard to install a listener here, even if there were a clear event to listen to
|
||||
const colorToDataURLCache = new Map();
|
||||
const colorToDataURLCache = new Map<string, string>();
|
||||
|
||||
export function defaultAvatarUrlForString(s) {
|
||||
export function defaultAvatarUrlForString(s: string): string {
|
||||
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
|
||||
const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
|
||||
let total = 0;
|
||||
|
@ -113,7 +118,7 @@ export function defaultAvatarUrlForString(s) {
|
|||
* @param {string} name
|
||||
* @return {string} the first letter
|
||||
*/
|
||||
export function getInitialLetter(name) {
|
||||
export function getInitialLetter(name: string): string {
|
||||
if (!name) {
|
||||
// XXX: We should find out what causes the name to sometimes be falsy.
|
||||
console.trace("`name` argument to `getInitialLetter` not supplied");
|
||||
|
@ -146,7 +151,7 @@ export function getInitialLetter(name) {
|
|||
return firstChar.toUpperCase();
|
||||
}
|
||||
|
||||
export function avatarUrlForRoom(room, width, height, resizeMethod) {
|
||||
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||
if (!room) return null; // null-guard
|
||||
|
||||
const explicitRoomAvatar = room.getAvatarUrl(
|
|
@ -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,7 +76,9 @@ 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";
|
||||
|
||||
enum AudioID {
|
||||
Ring = 'ringAudio',
|
||||
|
@ -96,6 +97,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>>();
|
||||
|
@ -179,8 +195,19 @@ export default class CallHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private matchesCallForThisRoom(call: MatrixCall) {
|
||||
// We don't allow placing more than one call per room, but that doesn't mean there
|
||||
// can't be more than one, eg. in a glare situation. This checks that the given call
|
||||
// is the call we consider 'the' call for its room.
|
||||
const callForThisRoom = this.getCallForRoom(call.roomId);
|
||||
return callForThisRoom && call.callId === callForThisRoom.callId;
|
||||
}
|
||||
|
||||
private setCallListeners(call: MatrixCall) {
|
||||
call.on(CallEvent.Error, (err) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
Analytics.trackEvent('voip', 'callError', 'error', err);
|
||||
console.error("Call error:", err);
|
||||
if (
|
||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||
|
@ -196,9 +223,15 @@ export default class CallHandler {
|
|||
});
|
||||
});
|
||||
call.on(CallEvent.Hangup, () => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
Analytics.trackEvent('voip', 'callHangup');
|
||||
|
||||
this.removeCallForRoom(call.roomId);
|
||||
});
|
||||
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
this.setCallState(call, newState);
|
||||
|
||||
switch (oldState) {
|
||||
|
@ -218,21 +251,63 @@ export default class CallHandler {
|
|||
this.play(AudioID.Ringback);
|
||||
break;
|
||||
case CallState.Ended:
|
||||
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
|
||||
this.removeCallForRoom(call.roomId);
|
||||
if (oldState === CallState.InviteSent && (
|
||||
call.hangupParty === CallParty.Remote ||
|
||||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||
)) {
|
||||
this.play(AudioID.Busy);
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
||||
title: _t('Call Timeout'),
|
||||
description: _t('The remote side failed to pick up') + '.',
|
||||
let title;
|
||||
let description;
|
||||
if (call.hangupReason === CallErrorCode.UserHangup) {
|
||||
title = _t("Call Declined");
|
||||
description = _t("The other party declined the call.");
|
||||
} else if (call.hangupReason === CallErrorCode.InviteTimeout) {
|
||||
title = _t("Call Failed");
|
||||
// XXX: full stop appended as some relic here, but these
|
||||
// strings need proper input from design anyway, so let's
|
||||
// not change this string until we have a proper one.
|
||||
description = _t('The remote side failed to pick up') + '.';
|
||||
} else {
|
||||
title = _t("Call Failed");
|
||||
description = _t("The call could not be established");
|
||||
}
|
||||
|
||||
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
|
||||
title, description,
|
||||
});
|
||||
} else 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
call.on(CallEvent.Replaced, (newCall: MatrixCall) => {
|
||||
if (!this.matchesCallForThisRoom(call)) return;
|
||||
|
||||
console.log(`Call ID ${call.callId} is being replaced by call ID ${newCall.callId}`);
|
||||
|
||||
if (call.state === CallState.Ringing) {
|
||||
this.pause(AudioID.Ring);
|
||||
} else if (call.state === CallState.InviteSent) {
|
||||
this.pause(AudioID.Ringback);
|
||||
}
|
||||
|
||||
this.calls.set(newCall.roomId, newCall);
|
||||
this.setCallListeners(newCall);
|
||||
this.setCallState(newCall, newCall.state);
|
||||
});
|
||||
}
|
||||
|
||||
private setCallAudioElement(call: MatrixCall) {
|
||||
const audioElement = getRemoteAudioElement();
|
||||
if (audioElement) call.setRemoteAudioElement(audioElement);
|
||||
}
|
||||
|
||||
private setCallState(call: MatrixCall, status: CallState) {
|
||||
|
@ -285,9 +360,13 @@ export default class CallHandler {
|
|||
roomId: string, type: PlaceCallType,
|
||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
||||
) {
|
||||
const call = Matrix.createNewMatrixCall(MatrixClientPeg.get(), roomId);
|
||||
Analytics.trackEvent('voip', 'placeCall', 'type', type);
|
||||
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') {
|
||||
|
@ -362,6 +441,8 @@ export default class CallHandler {
|
|||
break;
|
||||
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':
|
||||
|
@ -388,27 +469,37 @@ export default class CallHandler {
|
|||
}
|
||||
|
||||
const call = payload.call as MatrixCall;
|
||||
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||
this.calls.set(call.roomId, call)
|
||||
this.setCallListeners(call);
|
||||
this.setCallAudioElement(call);
|
||||
}
|
||||
break;
|
||||
case 'hangup':
|
||||
case 'reject':
|
||||
if (!this.calls.get(payload.room_id)) {
|
||||
return; // no call to hangup
|
||||
}
|
||||
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false)
|
||||
if (payload.action === 'reject') {
|
||||
this.calls.get(payload.room_id).reject();
|
||||
} else {
|
||||
this.calls.get(payload.room_id).hangup(CallErrorCode.UserHangup, false);
|
||||
}
|
||||
this.removeCallForRoom(payload.room_id);
|
||||
break;
|
||||
case 'answer':
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import extend from './extend';
|
||||
import dis from './dispatcher/dispatcher';
|
||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
|
@ -32,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;
|
||||
|
@ -369,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() {
|
||||
|
@ -480,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: {
|
||||
|
@ -497,7 +501,7 @@ export default class ContentMessages {
|
|||
if (file.type.indexOf('image/') === 0) {
|
||||
content.msgtype = 'm.image';
|
||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
||||
extend(content.info, imageInfo);
|
||||
Object.assign(content.info, imageInfo);
|
||||
resolve();
|
||||
}, (e) => {
|
||||
console.error(e);
|
||||
|
@ -510,7 +514,7 @@ export default class ContentMessages {
|
|||
} else if (file.type.indexOf('video/') === 0) {
|
||||
content.msgtype = 'm.video';
|
||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
|
||||
extend(content.info, videoInfo);
|
||||
Object.assign(content.info, videoInfo);
|
||||
resolve();
|
||||
}, (e) => {
|
||||
content.msgtype = 'm.file';
|
||||
|
@ -564,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) {
|
||||
|
|
|
@ -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;
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
function getDaysArray() {
|
||||
function getDaysArray(): string[] {
|
||||
return [
|
||||
_t('Sun'),
|
||||
_t('Mon'),
|
||||
|
@ -29,7 +29,7 @@ function getDaysArray() {
|
|||
];
|
||||
}
|
||||
|
||||
function getMonthsArray() {
|
||||
function getMonthsArray(): string[] {
|
||||
return [
|
||||
_t('Jan'),
|
||||
_t('Feb'),
|
||||
|
@ -46,11 +46,11 @@ function getMonthsArray() {
|
|||
];
|
||||
}
|
||||
|
||||
function pad(n) {
|
||||
function pad(n: number): string {
|
||||
return (n < 10 ? '0' : '') + n;
|
||||
}
|
||||
|
||||
function twelveHourTime(date, showSeconds=false) {
|
||||
function twelveHourTime(date: Date, showSeconds = false): string {
|
||||
let hours = date.getHours() % 12;
|
||||
const minutes = pad(date.getMinutes());
|
||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||
|
@ -62,7 +62,7 @@ function twelveHourTime(date, showSeconds=false) {
|
|||
return `${hours}:${minutes}${ampm}`;
|
||||
}
|
||||
|
||||
export function formatDate(date, showTwelveHour=false) {
|
||||
export function formatDate(date: Date, showTwelveHour = false): string {
|
||||
const now = new Date();
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
|
@ -86,7 +86,7 @@ export function formatDate(date, showTwelveHour=false) {
|
|||
return formatFullDate(date, showTwelveHour);
|
||||
}
|
||||
|
||||
export function formatFullDateNoTime(date) {
|
||||
export function formatFullDateNoTime(date: Date): string {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
|
||||
|
@ -97,7 +97,7 @@ export function formatFullDateNoTime(date) {
|
|||
});
|
||||
}
|
||||
|
||||
export function formatFullDate(date, showTwelveHour=false) {
|
||||
export function formatFullDate(date: Date, showTwelveHour = false): string {
|
||||
const days = getDaysArray();
|
||||
const months = getMonthsArray();
|
||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
||||
|
@ -109,14 +109,14 @@ export function formatFullDate(date, showTwelveHour=false) {
|
|||
});
|
||||
}
|
||||
|
||||
export function formatFullTime(date, showTwelveHour=false) {
|
||||
export function formatFullTime(date: Date, showTwelveHour = false): string {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date, true);
|
||||
}
|
||||
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
||||
}
|
||||
|
||||
export function formatTime(date, showTwelveHour=false) {
|
||||
export function formatTime(date: Date, showTwelveHour = false): string {
|
||||
if (showTwelveHour) {
|
||||
return twelveHourTime(date);
|
||||
}
|
||||
|
@ -124,7 +124,7 @@ export function formatTime(date, showTwelveHour=false) {
|
|||
}
|
||||
|
||||
const MILLIS_IN_DAY = 86400000;
|
||||
export function wantsDateSeparator(prevEventDate, nextEventDate) {
|
||||
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||
if (!nextEventDate || !prevEventDate) {
|
||||
return false;
|
||||
}
|
|
@ -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
|
|
@ -23,6 +23,7 @@ import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
|||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
import EventIndexPeg from './indexing/EventIndexPeg';
|
||||
import createMatrixClient from './utils/createMatrixClient';
|
||||
import Analytics from './Analytics';
|
||||
|
@ -46,6 +47,7 @@ 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";
|
||||
|
||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||
|
@ -567,6 +569,8 @@ function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void
|
|||
localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||
}
|
||||
|
||||
SecurityCustomisations.persistCredentials?.(credentials);
|
||||
|
||||
console.log(`Session persisted for ${credentials.userId}`);
|
||||
}
|
||||
|
||||
|
@ -577,6 +581,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
|
||||
|
|
|
@ -22,6 +22,7 @@ limitations under the License.
|
|||
import Matrix from "matrix-js-sdk";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
||||
interface ILoginOptions {
|
||||
defaultDeviceDisplayName?: string;
|
||||
|
@ -222,11 +223,15 @@ export async function sendLoginRequest(
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
const creds: IMatrixClientCreds = {
|
||||
homeserverUrl: hsUrl,
|
||||
identityServerUrl: isUrl,
|
||||
userId: data.user_id,
|
||||
deviceId: data.device_id,
|
||||
accessToken: data.access_token,
|
||||
};
|
||||
|
||||
SecurityCustomisations.examineLoginResponse?.(data, creds);
|
||||
|
||||
return creds;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -218,7 +218,7 @@ export const Notifier = {
|
|||
// calculated value. It is determined based upon whether or not the master rule is enabled
|
||||
// and other flags. Setting it here would cause a circular reference.
|
||||
|
||||
Analytics.trackEvent('Notifier', 'Set Enabled', enable);
|
||||
Analytics.trackEvent('Notifier', 'Set Enabled', String(enable));
|
||||
|
||||
// make sure that we persist the current setting audio_enabled setting
|
||||
// before changing anything
|
||||
|
@ -287,7 +287,7 @@ export const Notifier = {
|
|||
setPromptHidden: function(hidden: boolean, persistent = true) {
|
||||
this.toolbarHidden = hidden;
|
||||
|
||||
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
|
||||
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', String(hidden));
|
||||
|
||||
hideNotificationsToast();
|
||||
|
||||
|
|
|
@ -19,30 +19,34 @@ limitations under the License.
|
|||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import dis from "./dispatcher/dispatcher";
|
||||
import Timer from './utils/Timer';
|
||||
import {ActionPayload} from "./dispatcher/payloads";
|
||||
|
||||
// Time in ms after that a user is considered as unavailable/away
|
||||
// Time in ms after that a user is considered as unavailable/away
|
||||
const UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||
const PRESENCE_STATES = ["online", "offline", "unavailable"];
|
||||
|
||||
enum State {
|
||||
Online = "online",
|
||||
Offline = "offline",
|
||||
Unavailable = "unavailable",
|
||||
}
|
||||
|
||||
class Presence {
|
||||
constructor() {
|
||||
this._activitySignal = null;
|
||||
this._unavailableTimer = null;
|
||||
this._onAction = this._onAction.bind(this);
|
||||
this._dispatcherRef = null;
|
||||
}
|
||||
private unavailableTimer: Timer = null;
|
||||
private dispatcherRef: string = null;
|
||||
private state: State = null;
|
||||
|
||||
/**
|
||||
* Start listening the user activity to evaluate his presence state.
|
||||
* Any state change will be sent to the homeserver.
|
||||
*/
|
||||
async start() {
|
||||
this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
|
||||
public async start() {
|
||||
this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
|
||||
// the user_activity_start action starts the timer
|
||||
this._dispatcherRef = dis.register(this._onAction);
|
||||
while (this._unavailableTimer) {
|
||||
this.dispatcherRef = dis.register(this.onAction);
|
||||
while (this.unavailableTimer) {
|
||||
try {
|
||||
await this._unavailableTimer.finished();
|
||||
this.setState("unavailable");
|
||||
await this.unavailableTimer.finished();
|
||||
this.setState(State.Unavailable);
|
||||
} catch (e) { /* aborted, stop got called */ }
|
||||
}
|
||||
}
|
||||
|
@ -50,14 +54,14 @@ class Presence {
|
|||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
stop() {
|
||||
if (this._dispatcherRef) {
|
||||
dis.unregister(this._dispatcherRef);
|
||||
this._dispatcherRef = null;
|
||||
public stop() {
|
||||
if (this.dispatcherRef) {
|
||||
dis.unregister(this.dispatcherRef);
|
||||
this.dispatcherRef = null;
|
||||
}
|
||||
if (this._unavailableTimer) {
|
||||
this._unavailableTimer.abort();
|
||||
this._unavailableTimer = null;
|
||||
if (this.unavailableTimer) {
|
||||
this.unavailableTimer.abort();
|
||||
this.unavailableTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,14 +69,14 @@ class Presence {
|
|||
* Get the current presence state.
|
||||
* @returns {string} the presence state (see PRESENCE enum)
|
||||
*/
|
||||
getState() {
|
||||
public getState() {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
_onAction(payload) {
|
||||
private onAction = (payload: ActionPayload) => {
|
||||
if (payload.action === 'user_activity') {
|
||||
this.setState("online");
|
||||
this._unavailableTimer.restart();
|
||||
this.setState(State.Online);
|
||||
this.unavailableTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -81,13 +85,11 @@ class Presence {
|
|||
* If the state has changed, the homeserver will be notified.
|
||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||
*/
|
||||
async setState(newState) {
|
||||
private async setState(newState: State) {
|
||||
if (newState === this.state) {
|
||||
return;
|
||||
}
|
||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
||||
throw new Error("Bad presence state: " + newState);
|
||||
}
|
||||
|
||||
const oldState = this.state;
|
||||
this.state = newState;
|
||||
|
|
@ -13,9 +13,10 @@ 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 { _t } from './languageHandler';
|
||||
|
||||
export function levelRoleMap(usersDefault) {
|
||||
export function levelRoleMap(usersDefault: number) {
|
||||
return {
|
||||
undefined: _t('Default'),
|
||||
0: _t('Restricted'),
|
||||
|
@ -25,7 +26,7 @@ export function levelRoleMap(usersDefault) {
|
|||
};
|
||||
}
|
||||
|
||||
export function textualPowerLevel(level, usersDefault) {
|
||||
export function textualPowerLevel(level: number, usersDefault: number): string {
|
||||
const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
|
||||
if (LEVEL_ROLE_MAP[level]) {
|
||||
return LEVEL_ROLE_MAP[level];
|
|
@ -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];
|
||||
|
|
|
@ -360,7 +360,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
|||
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
||||
response.highlights = previousSearchResult.highlights;
|
||||
|
||||
if (localEvents && serverEvents) {
|
||||
if (localEvents && serverEvents && serverEvents.results) {
|
||||
// This is a first search call, combine the events from the server and
|
||||
// the local index. Note where our oldest event came from, we shall
|
||||
// fetch the next batch of events from the other source.
|
||||
|
@ -379,7 +379,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
|||
oldestEventFrom = "local";
|
||||
}
|
||||
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
|
||||
} else if (serverEvents) {
|
||||
} else if (serverEvents && serverEvents.results) {
|
||||
// This is a pagination call fetching more events from the server,
|
||||
// meaning that our oldest event was in the local index.
|
||||
// Change the source of the oldest event if our server event is older
|
||||
|
@ -454,7 +454,7 @@ function combineResponses(previousSearchResult, localEvents = undefined, serverE
|
|||
return response;
|
||||
}
|
||||
|
||||
function restoreEncryptionInfo(searchResultSlice) {
|
||||
function restoreEncryptionInfo(searchResultSlice = []) {
|
||||
for (let i = 0; i < searchResultSlice.length; i++) {
|
||||
const timeline = searchResultSlice[i].context.getTimeline();
|
||||
|
||||
|
@ -517,7 +517,7 @@ async function combinedPagination(searchResult) {
|
|||
},
|
||||
};
|
||||
|
||||
const oldResultCount = searchResult.results.length;
|
||||
const oldResultCount = searchResult.results ? searchResult.results.length : 0;
|
||||
|
||||
// Let the client process the combined result.
|
||||
const result = client._processRoomEventsSearch(searchResult, response);
|
||||
|
|
|
@ -22,11 +22,12 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
|||
import { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||
import { _t } from './languageHandler';
|
||||
import {encodeBase64} from "matrix-js-sdk/src/crypto/olmlib";
|
||||
import { encodeBase64 } from "matrix-js-sdk/src/crypto/olmlib";
|
||||
import { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
|
||||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import SecurityCustomisations from "./customisations/Security";
|
||||
|
||||
// This stores the secret storage private keys in memory for the JS SDK. This is
|
||||
// only meant to act as a cache to avoid prompting the user multiple times
|
||||
|
@ -115,6 +116,13 @@ async function getSecretStorageKey(
|
|||
}
|
||||
}
|
||||
|
||||
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||
if (keyFromCustomisations) {
|
||||
console.log("Using key from security customisations (secret storage)")
|
||||
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
|
||||
return [keyId, keyFromCustomisations];
|
||||
}
|
||||
|
||||
if (nonInteractive) {
|
||||
throw new Error("Could not unlock non-interactively");
|
||||
}
|
||||
|
@ -158,6 +166,12 @@ export async function getDehydrationKey(
|
|||
keyInfo: ISecretStorageKeyInfo,
|
||||
checkFunc: (Uint8Array) => void,
|
||||
): Promise<Uint8Array> {
|
||||
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||
if (keyFromCustomisations) {
|
||||
console.log("Using key from security customisations (dehydration)")
|
||||
return keyFromCustomisations;
|
||||
}
|
||||
|
||||
const inputToKey = makeInputToKey(keyInfo);
|
||||
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
||||
AccessSecretStorageDialog,
|
||||
|
@ -352,14 +366,19 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
|||
}
|
||||
console.log("Setting dehydration key");
|
||||
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
|
||||
} else if (!keyId) {
|
||||
console.warn("Not setting dehydration key: no SSSS key found");
|
||||
} else {
|
||||
console.log("Not setting dehydration key: no SSSS key found");
|
||||
console.log("Not setting dehydration key: feature disabled");
|
||||
}
|
||||
}
|
||||
|
||||
// `return await` needed here to ensure `finally` block runs after the
|
||||
// inner operation completes.
|
||||
return await func();
|
||||
} catch (e) {
|
||||
SecurityCustomisations.catchAccessSecretStorageError?.(e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
// Clear secret storage key cache now that work is complete
|
||||
secretStorageBeingAccessed = false;
|
||||
|
|
|
@ -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)}).`);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
|
|||
import SdkConfig from "./SdkConfig";
|
||||
import SettingsStore from "./settings/SettingsStore";
|
||||
import {UIFeature} from "./settings/UIFeature";
|
||||
import CallHandler from "./CallHandler";
|
||||
|
||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||
interface HTMLInputEvent extends Event {
|
||||
|
@ -517,6 +518,7 @@ export const Commands = [
|
|||
action: 'view_room',
|
||||
room_alias: roomAlias,
|
||||
auto_join: true,
|
||||
_type: "slash_command", // instrumentation
|
||||
});
|
||||
return success();
|
||||
} else if (params[0][0] === '!') {
|
||||
|
@ -531,6 +533,7 @@ export const Commands = [
|
|||
},
|
||||
via_servers: viaServers, // for the rejoin button
|
||||
auto_join: true,
|
||||
_type: "slash_command", // instrumentation
|
||||
});
|
||||
return success();
|
||||
} else if (isPermalink) {
|
||||
|
@ -555,6 +558,7 @@ export const Commands = [
|
|||
const dispatch = {
|
||||
action: 'view_room',
|
||||
auto_join: true,
|
||||
_type: "slash_command", // instrumentation
|
||||
};
|
||||
|
||||
if (entity[0] === '!') dispatch["room_id"] = entity;
|
||||
|
@ -998,14 +1002,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,
|
||||
});
|
||||
})());
|
||||
},
|
||||
|
@ -1039,6 +1058,32 @@ 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
|
||||
|
|
|
@ -198,59 +198,30 @@ function textForRelatedGroupsEvent(ev) {
|
|||
function textForServerACLEvent(ev) {
|
||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||
const prevContent = ev.getPrevContent();
|
||||
const changes = [];
|
||||
const current = ev.getContent();
|
||||
const prev = {
|
||||
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
|
||||
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
|
||||
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||
};
|
||||
|
||||
let text = "";
|
||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
||||
text = `${senderDisplayName} set server ACLs for this room: `;
|
||||
text = _t("%(senderDisplayName)s set the server ACLs for this room.", {senderDisplayName});
|
||||
} else {
|
||||
text = `${senderDisplayName} changed the server ACLs for this room: `;
|
||||
text = _t("%(senderDisplayName)s changed the server ACLs for this room.", {senderDisplayName});
|
||||
}
|
||||
|
||||
if (!Array.isArray(current.allow)) {
|
||||
current.allow = [];
|
||||
}
|
||||
/* If we know for sure everyone is banned, don't bother showing the diff view */
|
||||
|
||||
// If we know for sure everyone is banned, mark the room as obliterated
|
||||
if (current.allow.length === 0) {
|
||||
return text + "🎉 All servers are banned from participating! This room can no longer be used.";
|
||||
return text + " " + _t("🎉 All servers are banned from participating! This room can no longer be used.");
|
||||
}
|
||||
|
||||
if (!Array.isArray(current.deny)) {
|
||||
current.deny = [];
|
||||
}
|
||||
|
||||
const bannedServers = current.deny.filter((srv) => typeof(srv) === 'string' && !prev.deny.includes(srv));
|
||||
const unbannedServers = prev.deny.filter((srv) => typeof(srv) === 'string' && !current.deny.includes(srv));
|
||||
const allowedServers = current.allow.filter((srv) => typeof(srv) === 'string' && !prev.allow.includes(srv));
|
||||
const unallowedServers = prev.allow.filter((srv) => typeof(srv) === 'string' && !current.allow.includes(srv));
|
||||
|
||||
if (bannedServers.length > 0) {
|
||||
changes.push(`Servers matching ${bannedServers.join(", ")} are now banned.`);
|
||||
}
|
||||
|
||||
if (unbannedServers.length > 0) {
|
||||
changes.push(`Servers matching ${unbannedServers.join(", ")} were removed from the ban list.`);
|
||||
}
|
||||
|
||||
if (allowedServers.length > 0) {
|
||||
changes.push(`Servers matching ${allowedServers.join(", ")} are now allowed.`);
|
||||
}
|
||||
|
||||
if (unallowedServers.length > 0) {
|
||||
changes.push(`Servers matching ${unallowedServers.join(", ")} were removed from the allowed list.`);
|
||||
}
|
||||
|
||||
if (prev.allow_ip_literals !== current.allow_ip_literals) {
|
||||
const allowban = current.allow_ip_literals ? "allowed" : "banned";
|
||||
changes.push(`Participating from a server using an IP literal hostname is now ${allowban}.`);
|
||||
}
|
||||
|
||||
return text + changes.join(" ");
|
||||
return text;
|
||||
}
|
||||
|
||||
function textForMessageEvent(ev) {
|
||||
|
@ -329,14 +300,27 @@ function textForCallHangupEvent(event) {
|
|||
reason = _t('(not supported by this browser)');
|
||||
} else if (eventContent.reason) {
|
||||
if (eventContent.reason === "ice_failed") {
|
||||
// We couldn't establish a connection at all
|
||||
reason = _t('(could not connect media)');
|
||||
} else if (eventContent.reason === "ice_timeout") {
|
||||
// We established a connection but it died
|
||||
reason = _t('(connection failed)');
|
||||
} else if (eventContent.reason === "user_media_failed") {
|
||||
// The other side couldn't open capture devices
|
||||
reason = _t("(their device couldn't start the camera / microphone)");
|
||||
} else if (eventContent.reason === "unknown_error") {
|
||||
// An error code the other side doesn't have a way to express
|
||||
// (as opposed to an error code they gave but we don't know about,
|
||||
// in which case we show the error code)
|
||||
reason = _t("(an error occurred)");
|
||||
} else if (eventContent.reason === "invite_timeout") {
|
||||
reason = _t('(no answer)');
|
||||
} else if (eventContent.reason === "user hangup") {
|
||||
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
|
||||
// workaround for https://github.com/vector-im/element-web/issues/5178
|
||||
// it seems Android randomly sets a reason of "user hangup" which is
|
||||
// interpreted as an error code :(
|
||||
// https://github.com/vector-im/riot-android/issues/2623
|
||||
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||
reason = '';
|
||||
} else {
|
||||
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
||||
|
@ -345,6 +329,11 @@ function textForCallHangupEvent(event) {
|
|||
return _t('%(senderName)s ended the call.', {senderName}) + ' ' + reason;
|
||||
}
|
||||
|
||||
function textForCallRejectEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
return _t('%(senderName)s declined the call.', {senderName});
|
||||
}
|
||||
|
||||
function textForCallInviteEvent(event) {
|
||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||
// FIXME: Find a better way to determine this from the event?
|
||||
|
@ -574,6 +563,7 @@ const handlers = {
|
|||
'm.call.invite': textForCallInviteEvent,
|
||||
'm.call.answer': textForCallAnswerEvent,
|
||||
'm.call.hangup': textForCallHangupEvent,
|
||||
'm.call.reject': textForCallRejectEvent,
|
||||
};
|
||||
|
||||
const stateHandlers = {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -38,26 +38,23 @@ const RECENTLY_ACTIVE_THRESHOLD_MS = 2 * 60 * 1000;
|
|||
* see doc on the userActive* functions for what these mean.
|
||||
*/
|
||||
export default class UserActivity {
|
||||
constructor(windowObj, documentObj) {
|
||||
this._window = windowObj;
|
||||
this._document = documentObj;
|
||||
private readonly activeNowTimeout: Timer;
|
||||
private readonly activeRecentlyTimeout: Timer;
|
||||
private attachedActiveNowTimers: Timer[] = [];
|
||||
private attachedActiveRecentlyTimers: Timer[] = [];
|
||||
private lastScreenX = 0;
|
||||
private lastScreenY = 0;
|
||||
|
||||
this._attachedActiveNowTimers = [];
|
||||
this._attachedActiveRecentlyTimers = [];
|
||||
this._activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS);
|
||||
this._activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS);
|
||||
this._onUserActivity = this._onUserActivity.bind(this);
|
||||
this._onWindowBlurred = this._onWindowBlurred.bind(this);
|
||||
this._onPageVisibilityChanged = this._onPageVisibilityChanged.bind(this);
|
||||
this.lastScreenX = 0;
|
||||
this.lastScreenY = 0;
|
||||
constructor(private readonly window: Window, private readonly document: Document) {
|
||||
this.activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS);
|
||||
this.activeRecentlyTimeout = new Timer(RECENTLY_ACTIVE_THRESHOLD_MS);
|
||||
}
|
||||
|
||||
static sharedInstance() {
|
||||
if (global.mxUserActivity === undefined) {
|
||||
global.mxUserActivity = new UserActivity(window, document);
|
||||
if (window.mxUserActivity === undefined) {
|
||||
window.mxUserActivity = new UserActivity(window, document);
|
||||
}
|
||||
return global.mxUserActivity;
|
||||
return window.mxUserActivity;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -69,8 +66,8 @@ export default class UserActivity {
|
|||
* later on when the user does become active.
|
||||
* @param {Timer} timer the timer to use
|
||||
*/
|
||||
timeWhileActiveNow(timer) {
|
||||
this._timeWhile(timer, this._attachedActiveNowTimers);
|
||||
public timeWhileActiveNow(timer: Timer) {
|
||||
this.timeWhile(timer, this.attachedActiveNowTimers);
|
||||
if (this.userActiveNow()) {
|
||||
timer.start();
|
||||
}
|
||||
|
@ -85,14 +82,14 @@ export default class UserActivity {
|
|||
* later on when the user does become active.
|
||||
* @param {Timer} timer the timer to use
|
||||
*/
|
||||
timeWhileActiveRecently(timer) {
|
||||
this._timeWhile(timer, this._attachedActiveRecentlyTimers);
|
||||
public timeWhileActiveRecently(timer: Timer) {
|
||||
this.timeWhile(timer, this.attachedActiveRecentlyTimers);
|
||||
if (this.userActiveRecently()) {
|
||||
timer.start();
|
||||
}
|
||||
}
|
||||
|
||||
_timeWhile(timer, attachedTimers) {
|
||||
private timeWhile(timer: Timer, attachedTimers: Timer[]) {
|
||||
// important this happens first
|
||||
const index = attachedTimers.indexOf(timer);
|
||||
if (index === -1) {
|
||||
|
@ -112,36 +109,36 @@ export default class UserActivity {
|
|||
/**
|
||||
* Start listening to user activity
|
||||
*/
|
||||
start() {
|
||||
this._document.addEventListener('mousedown', this._onUserActivity);
|
||||
this._document.addEventListener('mousemove', this._onUserActivity);
|
||||
this._document.addEventListener('keydown', this._onUserActivity);
|
||||
this._document.addEventListener("visibilitychange", this._onPageVisibilityChanged);
|
||||
this._window.addEventListener("blur", this._onWindowBlurred);
|
||||
this._window.addEventListener("focus", this._onUserActivity);
|
||||
public start() {
|
||||
this.document.addEventListener('mousedown', this.onUserActivity);
|
||||
this.document.addEventListener('mousemove', this.onUserActivity);
|
||||
this.document.addEventListener('keydown', this.onUserActivity);
|
||||
this.document.addEventListener("visibilitychange", this.onPageVisibilityChanged);
|
||||
this.window.addEventListener("blur", this.onWindowBlurred);
|
||||
this.window.addEventListener("focus", this.onUserActivity);
|
||||
// can't use document.scroll here because that's only the document
|
||||
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||
// also this needs to be the wheel event, not scroll, as scroll is
|
||||
// fired when the view scrolls down for a new message.
|
||||
this._window.addEventListener('wheel', this._onUserActivity, {
|
||||
passive: true, capture: true,
|
||||
this.window.addEventListener('wheel', this.onUserActivity, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
stop() {
|
||||
this._document.removeEventListener('mousedown', this._onUserActivity);
|
||||
this._document.removeEventListener('mousemove', this._onUserActivity);
|
||||
this._document.removeEventListener('keydown', this._onUserActivity);
|
||||
this._window.removeEventListener('wheel', this._onUserActivity, {
|
||||
passive: true, capture: true,
|
||||
public stop() {
|
||||
this.document.removeEventListener('mousedown', this.onUserActivity);
|
||||
this.document.removeEventListener('mousemove', this.onUserActivity);
|
||||
this.document.removeEventListener('keydown', this.onUserActivity);
|
||||
this.window.removeEventListener('wheel', this.onUserActivity, {
|
||||
capture: true,
|
||||
});
|
||||
|
||||
this._document.removeEventListener("visibilitychange", this._onPageVisibilityChanged);
|
||||
this._window.removeEventListener("blur", this._onWindowBlurred);
|
||||
this._window.removeEventListener("focus", this._onUserActivity);
|
||||
this.document.removeEventListener("visibilitychange", this.onPageVisibilityChanged);
|
||||
this.window.removeEventListener("blur", this.onWindowBlurred);
|
||||
this.window.removeEventListener("focus", this.onUserActivity);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -151,8 +148,8 @@ export default class UserActivity {
|
|||
* user's attention at any given moment.
|
||||
* @returns {boolean} true if user is currently 'active'
|
||||
*/
|
||||
userActiveNow() {
|
||||
return this._activeNowTimeout.isRunning();
|
||||
public userActiveNow() {
|
||||
return this.activeNowTimeout.isRunning();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,27 +160,27 @@ export default class UserActivity {
|
|||
* (or they may have gone to make tea and left the window focused).
|
||||
* @returns {boolean} true if user has been active recently
|
||||
*/
|
||||
userActiveRecently() {
|
||||
return this._activeRecentlyTimeout.isRunning();
|
||||
public userActiveRecently() {
|
||||
return this.activeRecentlyTimeout.isRunning();
|
||||
}
|
||||
|
||||
_onPageVisibilityChanged(e) {
|
||||
if (this._document.visibilityState === "hidden") {
|
||||
this._activeNowTimeout.abort();
|
||||
this._activeRecentlyTimeout.abort();
|
||||
private onPageVisibilityChanged = e => {
|
||||
if (this.document.visibilityState === "hidden") {
|
||||
this.activeNowTimeout.abort();
|
||||
this.activeRecentlyTimeout.abort();
|
||||
} else {
|
||||
this._onUserActivity(e);
|
||||
this.onUserActivity(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onWindowBlurred() {
|
||||
this._activeNowTimeout.abort();
|
||||
this._activeRecentlyTimeout.abort();
|
||||
}
|
||||
private onWindowBlurred = () => {
|
||||
this.activeNowTimeout.abort();
|
||||
this.activeRecentlyTimeout.abort();
|
||||
};
|
||||
|
||||
_onUserActivity(event) {
|
||||
private onUserActivity = (event: MouseEvent) => {
|
||||
// ignore anything if the window isn't focused
|
||||
if (!this._document.hasFocus()) return;
|
||||
if (!this.document.hasFocus()) return;
|
||||
|
||||
if (event.screenX && event.type === "mousemove") {
|
||||
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
|
||||
|
@ -195,25 +192,25 @@ export default class UserActivity {
|
|||
}
|
||||
|
||||
dis.dispatch({action: 'user_activity'});
|
||||
if (!this._activeNowTimeout.isRunning()) {
|
||||
this._activeNowTimeout.start();
|
||||
if (!this.activeNowTimeout.isRunning()) {
|
||||
this.activeNowTimeout.start();
|
||||
dis.dispatch({action: 'user_activity_start'});
|
||||
|
||||
this._runTimersUntilTimeout(this._attachedActiveNowTimers, this._activeNowTimeout);
|
||||
UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout);
|
||||
} else {
|
||||
this._activeNowTimeout.restart();
|
||||
this.activeNowTimeout.restart();
|
||||
}
|
||||
|
||||
if (!this._activeRecentlyTimeout.isRunning()) {
|
||||
this._activeRecentlyTimeout.start();
|
||||
if (!this.activeRecentlyTimeout.isRunning()) {
|
||||
this.activeRecentlyTimeout.start();
|
||||
|
||||
this._runTimersUntilTimeout(this._attachedActiveRecentlyTimers, this._activeRecentlyTimeout);
|
||||
UserActivity.runTimersUntilTimeout(this.attachedActiveRecentlyTimers, this.activeRecentlyTimeout);
|
||||
} else {
|
||||
this._activeRecentlyTimeout.restart();
|
||||
this.activeRecentlyTimeout.restart();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
async _runTimersUntilTimeout(attachedTimers, timeout) {
|
||||
private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) {
|
||||
attachedTimers.forEach((t) => t.start());
|
||||
try {
|
||||
await timeout.finished();
|
|
@ -14,19 +14,18 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {RoomMember} from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||
import { _t } from './languageHandler';
|
||||
|
||||
export function usersTypingApartFromMeAndIgnored(room) {
|
||||
return usersTyping(
|
||||
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
|
||||
);
|
||||
export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] {
|
||||
return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers()));
|
||||
}
|
||||
|
||||
export function usersTypingApartFromMe(room) {
|
||||
return usersTyping(
|
||||
room, [MatrixClientPeg.get().credentials.userId],
|
||||
);
|
||||
export function usersTypingApartFromMe(room: Room): RoomMember[] {
|
||||
return usersTyping(room, [MatrixClientPeg.get().getUserId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,15 +33,11 @@ export function usersTypingApartFromMe(room) {
|
|||
* to exclude, return a list of user objects who are typing.
|
||||
* @param {Room} room: room object to get users from.
|
||||
* @param {string[]} exclude: list of user mxids to exclude.
|
||||
* @returns {string[]} list of user objects who are typing.
|
||||
* @returns {RoomMember[]} list of user objects who are typing.
|
||||
*/
|
||||
export function usersTyping(room, exclude) {
|
||||
export function usersTyping(room: Room, exclude: string[] = []): RoomMember[] {
|
||||
const whoIsTyping = [];
|
||||
|
||||
if (exclude === undefined) {
|
||||
exclude = [];
|
||||
}
|
||||
|
||||
const memberKeys = Object.keys(room.currentState.members);
|
||||
for (let i = 0; i < memberKeys.length; ++i) {
|
||||
const userId = memberKeys[i];
|
||||
|
@ -57,20 +52,21 @@ export function usersTyping(room, exclude) {
|
|||
return whoIsTyping;
|
||||
}
|
||||
|
||||
export function whoIsTypingString(whoIsTyping, limit) {
|
||||
export function whoIsTypingString(whoIsTyping: RoomMember[], limit: number): string {
|
||||
let othersCount = 0;
|
||||
if (whoIsTyping.length > limit) {
|
||||
othersCount = whoIsTyping.length - limit + 1;
|
||||
}
|
||||
|
||||
if (whoIsTyping.length === 0) {
|
||||
return '';
|
||||
} else if (whoIsTyping.length === 1) {
|
||||
return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name});
|
||||
}
|
||||
const names = whoIsTyping.map(function(m) {
|
||||
return m.name;
|
||||
});
|
||||
if (othersCount>=1) {
|
||||
|
||||
const names = whoIsTyping.map(m => m.name);
|
||||
|
||||
if (othersCount >= 1) {
|
||||
return _t('%(names)s and %(count)s others are typing …', {
|
||||
names: names.slice(0, limit - 1).join(', '),
|
||||
count: othersCount,
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
|||
|
||||
import Analytics from '../Analytics';
|
||||
import { asyncAction } from './actionCreators';
|
||||
import TagOrderStore from '../stores/TagOrderStore';
|
||||
import GroupFilterOrderStore from '../stores/GroupFilterOrderStore';
|
||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
export default class TagOrderActions {
|
||||
/**
|
||||
* Creates an action thunk that will do an asynchronous request to
|
||||
* move a tag in TagOrderStore to destinationIx.
|
||||
* move a tag in GroupFilterOrderStore to destinationIx.
|
||||
*
|
||||
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||
* account data on.
|
||||
|
@ -36,8 +36,8 @@ export default class TagOrderActions {
|
|||
*/
|
||||
public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload {
|
||||
// Only commit tags if the state is ready, i.e. not null
|
||||
let tags = TagOrderStore.getOrderedTags();
|
||||
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
let tags = GroupFilterOrderStore.getOrderedTags();
|
||||
let removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
@ -47,7 +47,7 @@ export default class TagOrderActions {
|
|||
|
||||
removedTags = removedTags.filter((t) => t !== tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
const storeId = GroupFilterOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.moveTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||
|
@ -83,8 +83,8 @@ export default class TagOrderActions {
|
|||
*/
|
||||
public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload {
|
||||
// Don't change tags, just removedTags
|
||||
const tags = TagOrderStore.getOrderedTags();
|
||||
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
||||
const tags = GroupFilterOrderStore.getOrderedTags();
|
||||
const removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
|
||||
|
||||
if (removedTags.includes(tag)) {
|
||||
// Return a thunk that doesn't do anything, we don't even need
|
||||
|
@ -94,7 +94,7 @@ export default class TagOrderActions {
|
|||
|
||||
removedTags.push(tag);
|
||||
|
||||
const storeId = TagOrderStore.getStoreId();
|
||||
const storeId = GroupFilterOrderStore.getStoreId();
|
||||
|
||||
return asyncAction('TagOrderActions.removeTag', () => {
|
||||
Analytics.trackEvent('TagOrderActions', 'removeTag');
|
||||
|
|
|
@ -32,6 +32,7 @@ import DialogButtons from "../../../../components/views/elements/DialogButtons";
|
|||
import InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
|
||||
import SecurityCustomisations from "../../../../customisations/Security";
|
||||
|
||||
const PHASE_LOADING = 0;
|
||||
const PHASE_LOADERROR = 1;
|
||||
|
@ -99,7 +100,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
|
||||
this._passphraseField = createRef();
|
||||
|
||||
this._fetchBackupInfo();
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
|
||||
if (this.state.accountPassword) {
|
||||
// If we have an account password in memory, let's simplify and
|
||||
// assume it means password auth is also supported for device
|
||||
|
@ -110,13 +112,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
|||
this._queryKeyUploadAuth();
|
||||
}
|
||||
|
||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
this._getInitialPhase();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||
}
|
||||
|
||||
_getInitialPhase() {
|
||||
const keyFromCustomisations = SecurityCustomisations.createSecretStorageKey?.();
|
||||
if (keyFromCustomisations) {
|
||||
console.log("Created key via customisations, jumping to bootstrap step");
|
||||
this._recoveryKey = {
|
||||
privateKey: keyFromCustomisations,
|
||||
};
|
||||
this._bootstrapSecretStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
this._fetchBackupInfo();
|
||||
}
|
||||
|
||||
async _fetchBackupInfo() {
|
||||
try {
|
||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||
|
@ -454,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">
|
||||
|
@ -472,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">
|
||||
|
@ -493,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>
|
||||
|
|
|
@ -416,8 +416,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
|||
return menuOptions;
|
||||
};
|
||||
|
||||
export const useContextMenu = (): [boolean, RefObject<HTMLElement>, () => void, () => void, (val: boolean) => void] => {
|
||||
const button = useRef<HTMLElement>(null);
|
||||
type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
|
||||
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
|
||||
const button = useRef<T>(null);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const open = () => {
|
||||
setIsOpen(true);
|
||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import TagOrderStore from '../../stores/TagOrderStore';
|
||||
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
||||
|
||||
import GroupActions from '../../actions/GroupActions';
|
||||
|
||||
|
@ -31,7 +31,7 @@ import AutoHideScrollbar from "./AutoHideScrollbar";
|
|||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import UserTagTile from "../views/elements/UserTagTile";
|
||||
|
||||
class TagPanel extends React.Component {
|
||||
class GroupFilterPanel extends React.Component {
|
||||
static contextType = MatrixClientContext;
|
||||
|
||||
state = {
|
||||
|
@ -44,13 +44,13 @@ class TagPanel extends React.Component {
|
|||
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.on("sync", this._onClientSync);
|
||||
|
||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
||||
this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
|
||||
if (this.unmounted) {
|
||||
return;
|
||||
}
|
||||
this.setState({
|
||||
orderedTags: TagOrderStore.getOrderedTags() || [],
|
||||
selectedTags: TagOrderStore.getSelectedTags(),
|
||||
orderedTags: GroupFilterOrderStore.getOrderedTags() || [],
|
||||
selectedTags: GroupFilterOrderStore.getSelectedTags(),
|
||||
});
|
||||
});
|
||||
// This could be done by anything with a matrix client
|
||||
|
@ -61,8 +61,8 @@ class TagPanel extends React.Component {
|
|||
this.unmounted = true;
|
||||
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||
this.context.removeListener("sync", this._onClientSync);
|
||||
if (this._tagOrderStoreToken) {
|
||||
this._tagOrderStoreToken.remove();
|
||||
if (this._groupFilterOrderStoreToken) {
|
||||
this._groupFilterOrderStoreToken.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -98,7 +98,7 @@ class TagPanel extends React.Component {
|
|||
return (
|
||||
<div>
|
||||
<UserTagTile />
|
||||
<hr className="mx_TagPanel_divider" />
|
||||
<hr className="mx_GroupFilterPanel_divider" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -117,8 +117,8 @@ class TagPanel extends React.Component {
|
|||
});
|
||||
|
||||
const itemsSelected = this.state.selectedTags.length > 0;
|
||||
const classes = classNames('mx_TagPanel', {
|
||||
mx_TagPanel_items_selected: itemsSelected,
|
||||
const classes = classNames('mx_GroupFilterPanel', {
|
||||
mx_GroupFilterPanel_items_selected: itemsSelected,
|
||||
});
|
||||
|
||||
let createButton = (
|
||||
|
@ -141,7 +141,7 @@ class TagPanel extends React.Component {
|
|||
|
||||
return <div className={classes} onClick={this.onClearFilterClick}>
|
||||
<AutoHideScrollbar
|
||||
className="mx_TagPanel_scroller"
|
||||
className="mx_GroupFilterPanel_scroller"
|
||||
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273
|
||||
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
|
||||
onMouseDown={this.onMouseDown}
|
||||
|
@ -152,7 +152,7 @@ class TagPanel extends React.Component {
|
|||
>
|
||||
{ (provided, snapshot) => (
|
||||
<div
|
||||
className="mx_TagPanel_tagTileContainer"
|
||||
className="mx_GroupFilterPanel_tagTileContainer"
|
||||
ref={provided.innerRef}
|
||||
>
|
||||
{ this.renderGlobalIcon() }
|
||||
|
@ -168,4 +168,4 @@ class TagPanel extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
export default TagPanel;
|
||||
export default GroupFilterPanel;
|
|
@ -620,7 +620,7 @@ export default class GroupView extends React.Component {
|
|||
profileForm: newProfileForm,
|
||||
|
||||
// Indicate that FlairStore needs to be poked to show this change
|
||||
// in TagTile (TagPanel), Flair and GroupTile (MyGroups).
|
||||
// in TagTile (GroupFilterPanel), Flair and GroupTile (MyGroups).
|
||||
avatarChanged: true,
|
||||
});
|
||||
}).catch((e) => {
|
||||
|
@ -649,7 +649,6 @@ export default class GroupView extends React.Component {
|
|||
editing: false,
|
||||
summary: null,
|
||||
});
|
||||
dis.dispatch({action: 'panel_disable'});
|
||||
this._initGroupStore(this.props.groupId);
|
||||
|
||||
if (this.state.avatarChanged) {
|
||||
|
@ -870,10 +869,7 @@ export default class GroupView extends React.Component {
|
|||
{ _t('Add rooms to this community') }
|
||||
</div>
|
||||
</AccessibleButton>) : <div />;
|
||||
const roomDetailListClassName = classnames({
|
||||
"mx_fadable": true,
|
||||
"mx_fadable_faded": this.state.editing,
|
||||
});
|
||||
|
||||
return <div className="mx_GroupView_rooms">
|
||||
<div className="mx_GroupView_rooms_header">
|
||||
<h3>
|
||||
|
@ -884,9 +880,7 @@ export default class GroupView extends React.Component {
|
|||
</div>
|
||||
{ this.state.groupRoomsLoading ?
|
||||
<Spinner /> :
|
||||
<RoomDetailList
|
||||
rooms={this.state.groupRooms}
|
||||
className={roomDetailListClassName} />
|
||||
<RoomDetailList rooms={this.state.groupRooms} />
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
|
|