Merge branch 'develop' into develop
|
@ -1,36 +1,15 @@
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||||
|
|
||||||
src/ImageUtils.js
|
|
||||||
src/Markdown.js
|
src/Markdown.js
|
||||||
src/Rooms.js
|
|
||||||
src/Unread.js
|
|
||||||
src/Velociraptor.js
|
src/Velociraptor.js
|
||||||
src/components/structures/RoomDirectory.js
|
src/components/structures/RoomDirectory.js
|
||||||
src/components/structures/ScrollPanel.js
|
|
||||||
src/components/structures/UploadBar.js
|
|
||||||
src/components/views/elements/AddressSelector.js
|
|
||||||
src/components/views/elements/DirectorySearchBox.js
|
|
||||||
src/components/views/messages/MFileBody.js
|
|
||||||
src/components/views/messages/TextualBody.js
|
|
||||||
src/components/views/rooms/AuxPanel.js
|
|
||||||
src/components/views/rooms/LinkPreviewWidget.js
|
|
||||||
src/components/views/rooms/MemberList.js
|
src/components/views/rooms/MemberList.js
|
||||||
src/components/views/rooms/RoomPreviewBar.js
|
|
||||||
src/components/views/settings/ChangeAvatar.js
|
|
||||||
src/components/views/settings/DevicesPanel.js
|
|
||||||
src/components/views/settings/Notifications.js
|
|
||||||
src/rageshake/rageshake.js
|
|
||||||
src/ratelimitedfunc.js
|
src/ratelimitedfunc.js
|
||||||
src/utils/DMRoomMap.js
|
src/utils/DMRoomMap.js
|
||||||
src/utils/DecryptFile.js
|
|
||||||
src/utils/DirectoryUtils.js
|
|
||||||
src/utils/MultiInviter.js
|
src/utils/MultiInviter.js
|
||||||
src/utils/Receipt.js
|
|
||||||
test/components/structures/MessagePanel-test.js
|
test/components/structures/MessagePanel-test.js
|
||||||
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
test/components/views/dialogs/InteractiveAuthDialog-test.js
|
||||||
test/mock-clock.js
|
test/mock-clock.js
|
||||||
test/notifications/ContentRules-test.js
|
|
||||||
test/notifications/PushRuleVectorState-test.js
|
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
test/end-to-end-tests/node_modules/
|
test/end-to-end-tests/node_modules/
|
||||||
test/end-to-end-tests/riot/
|
test/end-to-end-tests/riot/
|
||||||
|
|
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)
|
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)
|
[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/engage/element-web/?utm_source=widget)
|
[](https://translate.element.io/engage/element-web/?utm_source=widget)
|
||||||
|
|
||||||
Developer Guide
|
Developer Guide
|
||||||
===============
|
===============
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "matrix-react-sdk",
|
"name": "matrix-react-sdk",
|
||||||
"version": "3.6.0",
|
"version": "3.8.0",
|
||||||
"description": "SDK for matrix.org using React",
|
"description": "SDK for matrix.org using React",
|
||||||
"author": "matrix.org",
|
"author": "matrix.org",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
"linkifyjs": "^2.1.9",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.19",
|
"lodash": "^4.17.19",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"matrix-widget-api": "^0.1.0-beta.3",
|
"matrix-widget-api": "^0.1.0-beta.8",
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"pako": "^1.0.11",
|
"pako": "^1.0.11",
|
||||||
"parse5": "^5.1.1",
|
"parse5": "^5.1.1",
|
||||||
|
|
|
@ -32,9 +32,7 @@ do
|
||||||
echo "Upgrading $i to $latestver..."
|
echo "Upgrading $i to $latestver..."
|
||||||
yarn add -E $i@$latestver
|
yarn add -E $i@$latestver
|
||||||
git add -u
|
git add -u
|
||||||
# The `-e` flag opens the editor and gives you a chance to check
|
git commit -m "Upgrade $i to $latestver"
|
||||||
# the upgrade for correctness.
|
|
||||||
git commit -m "Upgrade $i to $latestver" -e
|
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "./_font-sizes.scss";
|
@import "./_font-sizes.scss";
|
||||||
|
@import "./_font-weights.scss";
|
||||||
|
|
||||||
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
$hover-transition: 0.08s cubic-bezier(.46, .03, .52, .96); // quadratic
|
||||||
|
|
||||||
|
@ -208,12 +209,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
border: 0;
|
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
|
// 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
|
// (which only have CSS, unlike skins) tell the app what their non-tinted
|
||||||
// colourscheme is by inspecting the stylesheet DOM.
|
// colourscheme is by inspecting the stylesheet DOM.
|
||||||
|
@ -329,6 +324,7 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
|
|
||||||
.mx_Dialog_title {
|
.mx_Dialog_title {
|
||||||
font-size: $font-22px;
|
font-size: $font-22px;
|
||||||
|
font-weight: $font-semi-bold;
|
||||||
line-height: $font-36px;
|
line-height: $font-36px;
|
||||||
color: $dialog-title-fg-color;
|
color: $dialog-title-fg-color;
|
||||||
}
|
}
|
||||||
|
@ -354,8 +350,8 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
|
||||||
background-color: $dialog-close-fg-color;
|
background-color: $dialog-close-fg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 4px;
|
top: 10px;
|
||||||
right: 0px;
|
right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Dialog_content {
|
.mx_Dialog_content {
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
@import "./structures/_CustomRoomTagPanel.scss";
|
@import "./structures/_CustomRoomTagPanel.scss";
|
||||||
@import "./structures/_FilePanel.scss";
|
@import "./structures/_FilePanel.scss";
|
||||||
@import "./structures/_GenericErrorPage.scss";
|
@import "./structures/_GenericErrorPage.scss";
|
||||||
|
@import "./structures/_GroupFilterPanel.scss";
|
||||||
@import "./structures/_GroupView.scss";
|
@import "./structures/_GroupView.scss";
|
||||||
@import "./structures/_HeaderButtons.scss";
|
@import "./structures/_HeaderButtons.scss";
|
||||||
@import "./structures/_HomePage.scss";
|
@import "./structures/_HomePage.scss";
|
||||||
@import "./structures/_LeftPanel.scss";
|
@import "./structures/_LeftPanel.scss";
|
||||||
|
@import "./structures/_LeftPanelWidget.scss";
|
||||||
@import "./structures/_MainSplit.scss";
|
@import "./structures/_MainSplit.scss";
|
||||||
@import "./structures/_MatrixChat.scss";
|
@import "./structures/_MatrixChat.scss";
|
||||||
@import "./structures/_MyGroups.scss";
|
@import "./structures/_MyGroups.scss";
|
||||||
|
@ -26,7 +28,6 @@
|
||||||
@import "./structures/_ScrollPanel.scss";
|
@import "./structures/_ScrollPanel.scss";
|
||||||
@import "./structures/_SearchBox.scss";
|
@import "./structures/_SearchBox.scss";
|
||||||
@import "./structures/_TabbedView.scss";
|
@import "./structures/_TabbedView.scss";
|
||||||
@import "./structures/_TagPanel.scss";
|
|
||||||
@import "./structures/_ToastContainer.scss";
|
@import "./structures/_ToastContainer.scss";
|
||||||
@import "./structures/_UploadBar.scss";
|
@import "./structures/_UploadBar.scss";
|
||||||
@import "./structures/_UserMenu.scss";
|
@import "./structures/_UserMenu.scss";
|
||||||
|
@ -51,11 +52,11 @@
|
||||||
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
@import "./views/avatars/_DecoratedRoomAvatar.scss";
|
||||||
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
@import "./views/avatars/_MemberStatusMessageAvatar.scss";
|
||||||
@import "./views/avatars/_PulsedAvatar.scss";
|
@import "./views/avatars/_PulsedAvatar.scss";
|
||||||
|
@import "./views/avatars/_WidgetAvatar.scss";
|
||||||
@import "./views/context_menus/_IconizedContextMenu.scss";
|
@import "./views/context_menus/_IconizedContextMenu.scss";
|
||||||
@import "./views/context_menus/_MessageContextMenu.scss";
|
@import "./views/context_menus/_MessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
@import "./views/context_menus/_StatusMessageContextMenu.scss";
|
||||||
@import "./views/context_menus/_TagTileContextMenu.scss";
|
@import "./views/context_menus/_TagTileContextMenu.scss";
|
||||||
@import "./views/context_menus/_WidgetContextMenu.scss";
|
|
||||||
@import "./views/dialogs/_AddressPickerDialog.scss";
|
@import "./views/dialogs/_AddressPickerDialog.scss";
|
||||||
@import "./views/dialogs/_Analytics.scss";
|
@import "./views/dialogs/_Analytics.scss";
|
||||||
@import "./views/dialogs/_BugReportDialog.scss";
|
@import "./views/dialogs/_BugReportDialog.scss";
|
||||||
|
@ -69,11 +70,13 @@
|
||||||
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
@import "./views/dialogs/_DeactivateAccountDialog.scss";
|
||||||
@import "./views/dialogs/_DevtoolsDialog.scss";
|
@import "./views/dialogs/_DevtoolsDialog.scss";
|
||||||
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
@import "./views/dialogs/_EditCommunityPrototypeDialog.scss";
|
||||||
|
@import "./views/dialogs/_FeedbackDialog.scss";
|
||||||
@import "./views/dialogs/_GroupAddressPicker.scss";
|
@import "./views/dialogs/_GroupAddressPicker.scss";
|
||||||
@import "./views/dialogs/_IncomingSasDialog.scss";
|
@import "./views/dialogs/_IncomingSasDialog.scss";
|
||||||
@import "./views/dialogs/_InviteDialog.scss";
|
@import "./views/dialogs/_InviteDialog.scss";
|
||||||
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
|
||||||
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
|
||||||
|
@import "./views/dialogs/_ModalWidgetDialog.scss";
|
||||||
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
@import "./views/dialogs/_NewSessionReviewDialog.scss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
@import "./views/dialogs/_RoomSettingsDialog.scss";
|
||||||
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
@import "./views/dialogs/_RoomSettingsDialogBridges.scss";
|
||||||
|
@ -107,11 +110,11 @@
|
||||||
@import "./views/elements/_EventListSummary.scss";
|
@import "./views/elements/_EventListSummary.scss";
|
||||||
@import "./views/elements/_Field.scss";
|
@import "./views/elements/_Field.scss";
|
||||||
@import "./views/elements/_FormButton.scss";
|
@import "./views/elements/_FormButton.scss";
|
||||||
@import "./views/elements/_IconButton.scss";
|
|
||||||
@import "./views/elements/_ImageView.scss";
|
@import "./views/elements/_ImageView.scss";
|
||||||
@import "./views/elements/_InfoTooltip.scss";
|
@import "./views/elements/_InfoTooltip.scss";
|
||||||
@import "./views/elements/_InlineSpinner.scss";
|
@import "./views/elements/_InlineSpinner.scss";
|
||||||
@import "./views/elements/_ManageIntegsButton.scss";
|
@import "./views/elements/_ManageIntegsButton.scss";
|
||||||
|
@import "./views/elements/_MiniAvatarUploader.scss";
|
||||||
@import "./views/elements/_PowerSelector.scss";
|
@import "./views/elements/_PowerSelector.scss";
|
||||||
@import "./views/elements/_ProgressBar.scss";
|
@import "./views/elements/_ProgressBar.scss";
|
||||||
@import "./views/elements/_QRCode.scss";
|
@import "./views/elements/_QRCode.scss";
|
||||||
|
@ -136,6 +139,7 @@
|
||||||
@import "./views/groups/_GroupUserSettings.scss";
|
@import "./views/groups/_GroupUserSettings.scss";
|
||||||
@import "./views/messages/_CreateEvent.scss";
|
@import "./views/messages/_CreateEvent.scss";
|
||||||
@import "./views/messages/_DateSeparator.scss";
|
@import "./views/messages/_DateSeparator.scss";
|
||||||
|
@import "./views/messages/_EventTileBubble.scss";
|
||||||
@import "./views/messages/_MEmoteBody.scss";
|
@import "./views/messages/_MEmoteBody.scss";
|
||||||
@import "./views/messages/_MFileBody.scss";
|
@import "./views/messages/_MFileBody.scss";
|
||||||
@import "./views/messages/_MImageBody.scss";
|
@import "./views/messages/_MImageBody.scss";
|
||||||
|
@ -179,6 +183,7 @@
|
||||||
@import "./views/rooms/_MemberList.scss";
|
@import "./views/rooms/_MemberList.scss";
|
||||||
@import "./views/rooms/_MessageComposer.scss";
|
@import "./views/rooms/_MessageComposer.scss";
|
||||||
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
@import "./views/rooms/_MessageComposerFormatBar.scss";
|
||||||
|
@import "./views/rooms/_NewRoomIntro.scss";
|
||||||
@import "./views/rooms/_NotificationBadge.scss";
|
@import "./views/rooms/_NotificationBadge.scss";
|
||||||
@import "./views/rooms/_PinnedEventTile.scss";
|
@import "./views/rooms/_PinnedEventTile.scss";
|
||||||
@import "./views/rooms/_PinnedEventsPanel.scss";
|
@import "./views/rooms/_PinnedEventsPanel.scss";
|
||||||
|
@ -222,8 +227,9 @@
|
||||||
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_SecurityUserSettingsTab.scss";
|
||||||
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
@import "./views/settings/tabs/user/_VoiceUserSettingsTab.scss";
|
||||||
@import "./views/terms/_InlineTermsAgreement.scss";
|
@import "./views/terms/_InlineTermsAgreement.scss";
|
||||||
|
@import "./views/toasts/_AnalyticsToast.scss";
|
||||||
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
@import "./views/toasts/_NonUrgentEchoFailureToast.scss";
|
||||||
@import "./views/verification/_VerificationShowSas.scss";
|
@import "./views/verification/_VerificationShowSas.scss";
|
||||||
@import "./views/voip/_CallContainer.scss";
|
@import "./views/voip/_CallContainer.scss";
|
||||||
@import "./views/voip/_CallView.scss";
|
@import "./views/voip/_CallView.scss";
|
||||||
@import "./views/voip/_VideoView.scss";
|
@import "./views/voip/_VideoFeed.scss";
|
||||||
|
|
|
@ -16,13 +16,8 @@ limitations under the License.
|
||||||
|
|
||||||
// TODO: Update design for custom tags to match new designs
|
// TODO: Update design for custom tags to match new designs
|
||||||
|
|
||||||
.mx_LeftPanel_tagPanelContainer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CustomRoomTagPanel {
|
.mx_CustomRoomTagPanel {
|
||||||
background-color: $tagpanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
max-height: 40vh;
|
max-height: 40vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_TagPanel {
|
.mx_GroupFilterPanel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: $tagpanel-bg-color;
|
background-color: $groupFilterPanel-bg-color;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -26,49 +26,49 @@ limitations under the License.
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel_items_selected {
|
.mx_GroupFilterPanel_items_selected {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagPanel_divider {
|
.mx_GroupFilterPanel .mx_GroupFilterPanel_divider {
|
||||||
height: 0px;
|
height: 0px;
|
||||||
width: 90%;
|
width: 90%;
|
||||||
border: none;
|
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;
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagPanel_tagTileContainer {
|
.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
padding-top: 6px;
|
padding-top: 6px;
|
||||||
}
|
}
|
||||||
.mx_TagPanel .mx_TagPanel_tagTileContainer > div {
|
.mx_GroupFilterPanel .mx_GroupFilterPanel_tagTileContainer > div {
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile {
|
.mx_GroupFilterPanel .mx_TagTile {
|
||||||
// opacity: 0.5;
|
// opacity: 0.5;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile.mx_TagTile_prototype {
|
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_prototype {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile:focus,
|
.mx_GroupFilterPanel .mx_TagTile:focus,
|
||||||
.mx_TagPanel .mx_TagTile:hover,
|
.mx_GroupFilterPanel .mx_TagTile:hover,
|
||||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected {
|
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected {
|
||||||
// opacity: 1;
|
// opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile.mx_TagTile_selected_prototype {
|
.mx_GroupFilterPanel .mx_TagTile.mx_TagTile_selected_prototype {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile_plus {
|
.mx_GroupFilterPanel .mx_TagTile_plus {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 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: '';
|
content: '';
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
|
@ -142,7 +142,7 @@ limitations under the License.
|
||||||
border-radius: 0 3px 3px 0;
|
border-radius: 0 3px 3px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel .mx_TagTile.mx_AccessibleButton:focus {
|
.mx_GroupFilterPanel .mx_TagTile.mx_AccessibleButton:focus {
|
||||||
filter: none;
|
filter: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,9 +26,10 @@ limitations under the License.
|
||||||
|
|
||||||
.mx_HomePage_default {
|
.mx_HomePage_default {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
.mx_HomePage_default_wrapper {
|
.mx_HomePage_default_wrapper {
|
||||||
padding: 25vh 0 12px;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
img {
|
||||||
|
@ -50,56 +51,54 @@ limitations under the License.
|
||||||
color: $muted-fg-color;
|
color: $muted-fg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader {
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_HomePage_default_buttons {
|
.mx_HomePage_default_buttons {
|
||||||
margin: 80px auto 0;
|
margin: 60px auto 0;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin
|
padding: 73px 8px 15px; // top: 20px top padding + 40px icon + 13px margin
|
||||||
|
|
||||||
width: 104px; // 120px - 2* 8px
|
width: 160px;
|
||||||
margin: 0 39px; // 55px - 2* 8px
|
height: 132px;
|
||||||
|
margin: 20px;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: $font-15px;
|
font-size: $font-15px;
|
||||||
line-height: $font-20px;
|
line-height: $font-20px;
|
||||||
color: $muted-fg-color;
|
color: #fff; // on all themes
|
||||||
|
background-color: $accent-color;
|
||||||
&:hover {
|
|
||||||
color: $accent-color;
|
|
||||||
background: rgba($accent-color, 0.06);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
top: 20px;
|
top: 20px;
|
||||||
left: 40px; // (120px-40px)/2
|
left: 60px; // (160px-40px)/2
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
|
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
background-color: $muted-fg-color;
|
background-color: #fff; // on all themes
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_HomePage_button_sendDm::before {
|
&.mx_HomePage_button_sendDm::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/message-circle.svg');
|
mask-image: url('$(res)/img/element-icons/feedback.svg');
|
||||||
}
|
}
|
||||||
&.mx_HomePage_button_explore::before {
|
&.mx_HomePage_button_explore::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/explore.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||||
}
|
}
|
||||||
&.mx_HomePage_button_createGroup::before {
|
&.mx_HomePage_button_createGroup::before {
|
||||||
mask-image: url('$(res)/img/feather-customised/group.svg');
|
mask-image: url('$(res)/img/element-icons/community-members.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,29 +14,30 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 {
|
.mx_LeftPanel {
|
||||||
background-color: $roomlist-bg-color;
|
background-color: $roomlist-bg-color;
|
||||||
min-width: 260px;
|
min-width: 260px;
|
||||||
max-width: 50%;
|
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;
|
display: flex;
|
||||||
|
|
||||||
.mx_LeftPanel_tagPanelContainer {
|
.mx_LeftPanel_GroupFilterPanelContainer {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-basis: $tagPanelWidth;
|
flex-basis: $groupFilterPanelWidth;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
// Create another flexbox so the TagPanel fills the container
|
// Create another flexbox so the GroupFilterPanel fills the container
|
||||||
display: flex;
|
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 {
|
.mx_LeftPanel_roomListContainer {
|
||||||
width: 100%;
|
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
|
// Note: The 'room list' in this context is actually everything that isn't the tag
|
||||||
// panel, such as the menu options, breadcrumbs, filtering, etc
|
// panel, such as the menu options, breadcrumbs, filtering, etc
|
||||||
.mx_LeftPanel_roomListContainer {
|
.mx_LeftPanel_roomListContainer {
|
||||||
width: calc(100% - $tagPanelWidth);
|
width: calc(100% - $groupFilterPanelWidth);
|
||||||
background-color: $roomlist-bg-color;
|
background-color: $roomlist-bg-color;
|
||||||
|
|
||||||
// Create another flexbox (this time a column) for the room list components
|
// 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;
|
min-width: unset;
|
||||||
|
|
||||||
// We have to forcefully set the width to override the resizer's style attribute.
|
// We have to forcefully set the width to override the resizer's style attribute.
|
||||||
&.mx_LeftPanel_hasTagPanel {
|
&.mx_LeftPanel_hasGroupFilterPanel {
|
||||||
width: calc(68px + $tagPanelWidth) !important;
|
width: calc(68px + $groupFilterPanelWidth) !important;
|
||||||
}
|
}
|
||||||
&:not(.mx_LeftPanel_hasTagPanel) {
|
&:not(.mx_LeftPanel_hasGroupFilterPanel) {
|
||||||
width: 68px !important;
|
width: 68px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
145
res/css/structures/_LeftPanelWidget.scss
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget {
|
||||||
|
// largely based on RoomSublist
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_headerContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
height: 24px;
|
||||||
|
color: $roomlist-header-color;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_stickable {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_headerText {
|
||||||
|
flex: 1;
|
||||||
|
max-width: calc(100% - 16px);
|
||||||
|
line-height: $font-16px;
|
||||||
|
font-size: $font-13px;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
// Ellipsize any text overflow
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_collapseBtn {
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
margin-right: 6px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
position: absolute;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
background-color: $roomlist-header-color;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_LeftPanelWidget_collapseBtn_collapsed::before {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_resizeBox {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: visible; // let the resize handle out
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AppTileFullWidth {
|
||||||
|
flex: 1 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
// need this to be flex otherwise the overflow hidden from above
|
||||||
|
// sometimes vertically centers the clipped list ... no idea why it would do this
|
||||||
|
// as the box model should be top aligned. Happens in both FF and Chromium
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
mask-image: linear-gradient(0deg, transparent, black 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_resizerHandle {
|
||||||
|
cursor: ns-resize;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
// Override styles from library
|
||||||
|
width: unset !important;
|
||||||
|
height: 4px !important;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: -24px !important; // override from library - puts it in the margin-top of the headerContainer
|
||||||
|
|
||||||
|
// Together, these make the bar 64px wide
|
||||||
|
// These are also overridden from the library
|
||||||
|
left: calc(50% - 32px) !important;
|
||||||
|
right: calc(50% - 32px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover .mx_LeftPanelWidget_resizerHandle {
|
||||||
|
opacity: 0.8;
|
||||||
|
background-color: $primary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_maximizeButton {
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 7px;
|
||||||
|
position: relative;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 32px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/feather-customised/widget/maximise.svg');
|
||||||
|
background: $muted-fg-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_LeftPanelWidget_maximizeButtonTooltip {
|
||||||
|
margin-top: -3px;
|
||||||
|
}
|
|
@ -79,7 +79,6 @@ limitations under the License.
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MatrixChat > .mx_LeftPanel2:hover + .mx_ResizeHandle_horizontal,
|
|
||||||
.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover {
|
.mx_MatrixChat > .mx_ResizeHandle_horizontal:hover {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
|
|
@ -153,16 +153,6 @@ limitations under the License.
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomStatusBar_isAlone {
|
|
||||||
height: 50px;
|
|
||||||
line-height: $font-50px;
|
|
||||||
|
|
||||||
color: $primary-fg-color;
|
|
||||||
opacity: 0.5;
|
|
||||||
overflow-y: hidden;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MatrixChat_useCompactLayout {
|
.mx_MatrixChat_useCompactLayout {
|
||||||
.mx_RoomStatusBar {
|
.mx_RoomStatusBar {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
|
|
|
@ -230,6 +230,10 @@ limitations under the License.
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_UserMenu_contextMenu_hostingLink {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_IconizedContextMenu_icon {
|
.mx_IconizedContextMenu_icon {
|
||||||
|
|
|
@ -14,6 +14,35 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.mx_InteractiveAuthEntryComponents_emailWrapper {
|
||||||
|
padding-right: 60px;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 32px;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
position: absolute;
|
||||||
|
width: 116px;
|
||||||
|
height: 116px;
|
||||||
|
content: "";
|
||||||
|
right: -10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: rgba(244, 246, 250, 0.91);
|
||||||
|
border-radius: 50%;
|
||||||
|
top: -20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-image: url('$(res)/img/element-icons/email-prompt.svg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
top: -25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InteractiveAuthEntryComponents_msisdnWrapper {
|
.mx_InteractiveAuthEntryComponents_msisdnWrapper {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ActionPayload } from "../payloads";
|
.mx_WidgetAvatar {
|
||||||
import { Action } from "../actions";
|
border-radius: 4px;
|
||||||
|
|
||||||
export interface AppTileActionPayload extends ActionPayload {
|
|
||||||
action: Action.AppTileDelete | Action.AppTileRevoke;
|
|
||||||
widgetId: string;
|
|
||||||
}
|
}
|
121
res/css/views/dialogs/_FeedbackDialog.scss
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_FeedbackDialog {
|
||||||
|
hr {
|
||||||
|
margin: 24px 0;
|
||||||
|
border-color: $input-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_Dialog_content {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_FeedbackDialog_section {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 52px;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
color: $tertiary-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
a, .mx_AccessibleButton_kind_link {
|
||||||
|
color: $accent-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $icon-button-color;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background: $avatar-initial-color; // TODO
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: 24px;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_FeedbackDialog_reportBug {
|
||||||
|
&::after {
|
||||||
|
mask-image: url('$(res)/img/feather-customised/bug.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_FeedbackDialog_rateApp {
|
||||||
|
.mx_RadioButton {
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 20px;
|
||||||
|
transition: font-size 1s, border .5s;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
margin-top: 12px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
vertical-align: top;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
input[type="radio"] + div {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_content {
|
||||||
|
background: $icon-button-color;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 40px;
|
||||||
|
border-radius: 20px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_spacer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
& + .mx_RadioButton {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RadioButton_checked {
|
||||||
|
font-size: 24px;
|
||||||
|
border-color: $accent-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
mask-image: url('$(res)/img/element-icons/feedback.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -27,37 +27,29 @@ limitations under the License.
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
.mx_InviteDialog_userTile {
|
.mx_InviteDialog_userTile {
|
||||||
|
margin: 6px 6px 0 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: left;
|
min-width: max-content; // prevent manipulation by flexbox
|
||||||
position: relative;
|
|
||||||
top: 7px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Using a textarea for this element, to circumvent autofill
|
// Mostly copied from AddressPickerDialog; overrides bunch of our default text input styles
|
||||||
// Mostly copied from AddressPickerDialog
|
> input[type="text"] {
|
||||||
textarea,
|
margin: 6px 0 !important;
|
||||||
textarea:focus {
|
height: 24px;
|
||||||
height: 34px;
|
line-height: $font-24px;
|
||||||
line-height: $font-34px;
|
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
margin: 0 !important;
|
|
||||||
border: 0 !important;
|
border: 0 !important;
|
||||||
outline: 0 !important;
|
outline: 0 !important;
|
||||||
resize: none;
|
resize: none;
|
||||||
overflow: hidden;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
word-wrap: nowrap;
|
min-width: 40%;
|
||||||
|
flex: 1 !important;
|
||||||
// Roughly fill about 2/5ths of the available space. This is to try and 'fill' the
|
color: $primary-fg-color !important;
|
||||||
// remaining space after a bunch of pills, but is a bit hacky. Ideally we'd have
|
|
||||||
// support for "fill remaining width", but traditional tricks don't work with what
|
|
||||||
// we're pushing into this "field". Flexbox just makes things worse. The theory is
|
|
||||||
// that users won't need more than about 2/5ths of the input to find the person
|
|
||||||
// they're looking for.
|
|
||||||
width: 40%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +140,10 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_InviteDialog_roomTile_nameStack {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.mx_InviteDialog_roomTile_name {
|
.mx_InviteDialog_roomTile_name {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
|
|
|
@ -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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,23 +14,29 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_WidgetContextMenu {
|
.mx_ModalWidgetDialog {
|
||||||
padding: 6px;
|
.mx_ModalWidgetDialog_warning {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
.mx_WidgetContextMenu_option {
|
> img {
|
||||||
padding: 3px 6px 3px 6px;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
margin-right: 8px;
|
||||||
white-space: nowrap;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_WidgetContextMenu_separator {
|
.mx_ModalWidgetDialog_buttons {
|
||||||
margin-top: 0;
|
float: right;
|
||||||
margin-bottom: 0;
|
margin-top: 24px;
|
||||||
border-bottom-style: none;
|
|
||||||
border-left-style: none;
|
.mx_AccessibleButton + .mx_AccessibleButton {
|
||||||
border-right-style: none;
|
margin-left: 8px;
|
||||||
border-top-style: solid;
|
}
|
||||||
border-top-width: 1px;
|
}
|
||||||
border-color: $menu-border-color;
|
|
||||||
|
iframe {
|
||||||
|
width: 100%;
|
||||||
|
height: 450px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -25,7 +25,7 @@ limitations under the License.
|
||||||
.mx_AccessibleButton_hasKind {
|
.mx_AccessibleButton_hasKind {
|
||||||
padding: 7px 18px;
|
padding: 7px 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: $font-14px;
|
font-size: $font-14px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
.mx_IconButton {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 100%;
|
|
||||||
background-color: $accent-bg-color;
|
|
||||||
// don't shrink or grow if in a flex container
|
|
||||||
flex: 0 0 auto;
|
|
||||||
|
|
||||||
&.mx_AccessibleButton_disabled {
|
|
||||||
background-color: none;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: lightgrey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: 55%;
|
|
||||||
background-color: $accent-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_IconButton_icon_check::before {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/check.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mx_IconButton_icon_edit::before {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/edit.svg');
|
|
||||||
}
|
|
||||||
}
|
|
56
res/css/views/elements/_MiniAvatarUploader.scss
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader {
|
||||||
|
position: relative;
|
||||||
|
width: min-content;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
height: 26px;
|
||||||
|
width: 26px;
|
||||||
|
|
||||||
|
right: -6px;
|
||||||
|
bottom: -6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background-color: $primary-bg-color;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
background-color: $secondary-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-image: url('$(res)/img/element-icons/camera.svg');
|
||||||
|
mask-size: 16px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_MiniAvatarUploader_busy::after {
|
||||||
|
background: url("$(res)/img/spinner.gif") no-repeat center;
|
||||||
|
background-size: 80%;
|
||||||
|
mask: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader_input {
|
||||||
|
display: none;
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,25 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_CreateEvent {
|
.mx_CreateEvent {
|
||||||
background-color: $info-plinth-bg-color;
|
&::before {
|
||||||
padding-left: 20px;
|
background-color: $composer-e2e-icon-color;
|
||||||
padding-right: 20px;
|
mask-image: url('$(res)/img/element-icons/chat-bubbles.svg');
|
||||||
padding-top: 10px;
|
}
|
||||||
padding-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateEvent_image {
|
|
||||||
float: left;
|
|
||||||
margin-right: 20px;
|
|
||||||
width: 72px;
|
|
||||||
height: 34px;
|
|
||||||
|
|
||||||
background-color: $primary-fg-color;
|
|
||||||
mask: url('$(res)/img/room-continuation.svg');
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_CreateEvent_header {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
60
res/css/views/messages/_EventTileBubble.scss
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_EventTileBubble {
|
||||||
|
background-color: $dark-panel-bg-color;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 75%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
||||||
|
|
||||||
|
&::before, &::after {
|
||||||
|
position: relative;
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1 / 3;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-position: center;
|
||||||
|
mask-size: contain;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTileBubble_title, .mx_EventTileBubble_subtitle {
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTileBubble_title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: $font-15px;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_EventTileBubble_subtitle {
|
||||||
|
font-size: $font-12px;
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,41 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent {
|
.mx_MJitsiWidgetEvent {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
content: "";
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
|
||||||
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
background-color: $composer-e2e-icon-color; // XXX: Variable abuse
|
||||||
margin-top: 4px;
|
|
||||||
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent_title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: $font-15px;
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent_subtitle {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_MJitsiWidgetEvent_title,
|
|
||||||
.mx_MJitsiWidgetEvent_subtitle {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,28 +15,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_cryptoEvent {
|
.mx_cryptoEvent {
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 24px minmax(0, 1fr) min-content;
|
|
||||||
|
|
||||||
&.mx_cryptoEvent_icon::before,
|
|
||||||
&.mx_cryptoEvent_icon::after {
|
|
||||||
grid-column: 1;
|
|
||||||
grid-row: 1 / 3;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
content: "";
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
mask-repeat: no-repeat;
|
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
|
||||||
mask-image: url('$(res)/img/e2e/normal.svg');
|
|
||||||
background-color: $composer-e2e-icon-color;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// white infill for the transparency
|
// white infill for the transparency
|
||||||
&.mx_cryptoEvent_icon::before {
|
&.mx_cryptoEvent_icon::before {
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
@ -46,6 +24,11 @@ limitations under the License.
|
||||||
mask-size: 90%;
|
mask-size: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.mx_cryptoEvent_icon::after {
|
||||||
|
mask-image: url('$(res)/img/e2e/normal.svg');
|
||||||
|
background-color: $composer-e2e-icon-color;
|
||||||
|
}
|
||||||
|
|
||||||
&.mx_cryptoEvent_icon_verified::after {
|
&.mx_cryptoEvent_icon_verified::after {
|
||||||
mask-image: url("$(res)/img/e2e/verified.svg");
|
mask-image: url("$(res)/img/e2e/verified.svg");
|
||||||
background-color: $accent-color;
|
background-color: $accent-color;
|
||||||
|
@ -56,25 +39,6 @@ limitations under the License.
|
||||||
background-color: $notice-primary-color;
|
background-color: $notice-primary-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_cryptoEvent_title, .mx_cryptoEvent_subtitle, .mx_cryptoEvent_state {
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_title {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: $font-15px;
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_subtitle {
|
|
||||||
grid-column: 2;
|
|
||||||
grid-row: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_state, .mx_cryptoEvent_subtitle {
|
|
||||||
font-size: $font-12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
|
.mx_cryptoEvent_state, .mx_cryptoEvent_buttons {
|
||||||
grid-column: 3;
|
grid-column: 3;
|
||||||
|
@ -92,5 +56,7 @@ limitations under the License.
|
||||||
margin: auto 0;
|
margin: auto 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: $notice-secondary-color;
|
color: $notice-secondary-color;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
font-size: $font-12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -128,6 +128,13 @@ limitations under the License.
|
||||||
mask-size: 20px;
|
mask-size: 20px;
|
||||||
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
|
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_appsGroup {
|
||||||
.mx_RoomSummaryCard_Button {
|
.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;
|
color: $tertiary-fg-color;
|
||||||
|
|
||||||
span {
|
.mx_RoomSummaryCard_icon_app {
|
||||||
color: $primary-fg-color;
|
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 {
|
.mx_RoomSummaryCard_app_pinToggle,
|
||||||
vertical-align: top;
|
.mx_RoomSummaryCard_app_options {
|
||||||
margin-right: 12px;
|
position: absolute;
|
||||||
border-radius: 4px;
|
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 {
|
&::before {
|
||||||
content: unset;
|
content: unset;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomSummaryCard_icon_app_pinned::after {
|
&::after {
|
||||||
mask-image: url('$(res)/img/element-icons/room/pin-upright.svg');
|
top: 8px; // re-align based on the height change
|
||||||
background-color: $accent-color;
|
pointer-events: none; // pass through to the real button
|
||||||
transform: unset;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,26 +173,12 @@ limitations under the License.
|
||||||
|
|
||||||
margin: 6px 0;
|
margin: 6px 0;
|
||||||
|
|
||||||
.mx_IconButton, .mx_Spinner {
|
|
||||||
margin-left: 20px;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
mask-size: 80%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_UserInfo_roleDescription {
|
.mx_UserInfo_roleDescription {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
// try to make it the same height as the dropdown
|
// try to make it the same height as the dropdown
|
||||||
margin: 11px 0 12px 0;
|
margin: 11px 0 12px 0;
|
||||||
|
|
||||||
.mx_IconButton {
|
|
||||||
margin-left: 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_Field {
|
.mx_Field {
|
||||||
|
|
|
@ -24,34 +24,35 @@ limitations under the License.
|
||||||
border: 0;
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_WidgetCard_noEdit {
|
.mx_BaseCard_header {
|
||||||
.mx_AccessibleButton_kind_secondary {
|
display: inline-flex;
|
||||||
margin: 0 12px;
|
|
||||||
|
|
||||||
&:first-child {
|
& > h2 {
|
||||||
// expand the Pin to room primary action
|
margin-right: 0;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.mx_WidgetCard_optionsButton {
|
.mx_WidgetCard_optionsButton {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 18px;
|
margin-right: 44px;
|
||||||
width: 26px;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 20px;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
top: 6px;
|
width: 20px;
|
||||||
left: 20px;
|
min-width: 20px; // prevent crushing by the flexbox
|
||||||
mask-repeat: no-repeat;
|
padding: 0;
|
||||||
mask-position: center;
|
|
||||||
mask-size: contain;
|
&::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
content: "";
|
||||||
background-color: $secondary-fg-color;
|
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;
|
opacity: 0.8;
|
||||||
background: $primary-fg-color;
|
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 {
|
.mx_AppsContainer_resizer {
|
||||||
display: none;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsContainer {
|
.mx_AppsContainer {
|
||||||
|
@ -60,53 +76,71 @@ $MiniAppTileHeight: 200px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 100%;
|
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 {
|
// TODO this should be 300px but that's too large
|
||||||
// override the re-resizable inline styles
|
$MinWidth: 240px;
|
||||||
height: inherit !important;
|
|
||||||
min-height: inherit !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AddWidget_button {
|
.mx_AppsDrawer_2apps .mx_AppTile {
|
||||||
order: 2;
|
width: 50%;
|
||||||
cursor: pointer;
|
|
||||||
padding: 0;
|
|
||||||
margin: -3px auto 5px 0;
|
|
||||||
color: $accent-color;
|
|
||||||
font-size: $font-12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_SetAppURLDialog_input {
|
&:nth-child(3) {
|
||||||
border-radius: 3px;
|
flex-grow: 1;
|
||||||
border: 1px solid $input-border-color;
|
width: 0 !important;
|
||||||
padding: 9px;
|
min-width: $MinWidth !important;
|
||||||
color: $primary-hairline-color;
|
}
|
||||||
background-color: $primary-bg-color;
|
}
|
||||||
font-size: $font-15px;
|
.mx_AppsDrawer_3apps .mx_AppTile {
|
||||||
|
width: 33%;
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 0 !important;
|
||||||
|
min-width: $MinWidth !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile {
|
.mx_AppTile {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
min-width: $MinWidth;
|
||||||
border-radius: 4px;
|
border: 8px solid $widget-menu-bar-bg-color;
|
||||||
|
border-left-width: 5px;
|
||||||
|
border-right-width: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
& + .mx_AppTile {
|
background-color: $widget-menu-bar-bg-color;
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileFullWidth {
|
.mx_AppTileFullWidth {
|
||||||
width: 100%;
|
width: 100% !important; // to override the inline style set by the resizer
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border: 5px solid $widget-menu-bar-bg-color;
|
border: 5px solid $widget-menu-bar-bg-color;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
background-color: $widget-menu-bar-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTile_mini {
|
.mx_AppTile_mini {
|
||||||
|
@ -118,12 +152,6 @@ $MiniAppTileHeight: 200px;
|
||||||
height: $MiniAppTileHeight;
|
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_AppTile .mx_AppTile_persistedWrapper,
|
||||||
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
.mx_AppTileFullWidth .mx_AppTile_persistedWrapper,
|
||||||
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
.mx_AppTile_mini .mx_AppTile_persistedWrapper {
|
||||||
|
@ -143,19 +171,20 @@ $MiniAppTileHeight: 200px;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
cursor: pointer;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
padding-top: 2px;
|
||||||
|
padding-bottom: 8px;
|
||||||
.mx_AppTileMenuBar_expanded {
|
|
||||||
padding-bottom: 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBarTitle {
|
.mx_AppTileMenuBarTitle {
|
||||||
display: flex;
|
line-height: 20px;
|
||||||
flex-direction: row;
|
white-space: nowrap;
|
||||||
align-items: center;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
|
.mx_WidgetAvatar {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBarTitle > :last-child {
|
.mx_AppTileMenuBarTitle > :last-child {
|
||||||
|
@ -179,37 +208,20 @@ $MiniAppTileHeight: 200px;
|
||||||
margin: 0 3px;
|
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 {
|
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_popout {
|
||||||
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
mask-image: url('$(res)/img/feather-customised/widget/external-link.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
.mx_AppTileMenuBar_iconButton.mx_AppTileMenuBar_iconButton_menu {
|
||||||
mask-image: url('$(res)/img/icon_context.svg');
|
mask-image: url('$(res)/img/element-icons/room/ellipsis.svg');
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidgetDelete {
|
|
||||||
filter: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_AppTileMenuBarWidget:hover {
|
|
||||||
border: 1px solid $primary-fg-color;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileBody {
|
.mx_AppTileBody {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: $widget-body-bg-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppTileBody_mini {
|
.mx_AppTileBody_mini {
|
||||||
|
@ -242,75 +254,8 @@ $MiniAppTileHeight: 200px;
|
||||||
display: block;
|
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 {
|
.mx_AppPermissionWarning {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: $widget-menu-bar-bg-color;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -375,6 +320,10 @@ form.mx_Custom_Widget_Form div {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 100%;
|
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 {
|
.mx_AppLoading .mx_Spinner {
|
||||||
|
@ -402,10 +351,6 @@ form.mx_Custom_Widget_Form div {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AppsDrawer_minimised .mx_AppsContainer_resizerHandle {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Avoid apptile iframes capturing mouse event focus when resizing */
|
/* Avoid apptile iframes capturing mouse event focus when resizing */
|
||||||
.mx_AppsDrawer_resizing iframe {
|
.mx_AppsDrawer_resizing iframe {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
|
@ -25,15 +25,6 @@ $left-gutter: 64px;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_bubble {
|
|
||||||
background-color: $dark-panel-bg-color;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin: 10px auto;
|
|
||||||
max-width: 75%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_EventTile.mx_EventTile_info {
|
.mx_EventTile.mx_EventTile_info {
|
||||||
padding-top: 0px;
|
padding-top: 0px;
|
||||||
}
|
}
|
||||||
|
@ -131,9 +122,10 @@ $left-gutter: 64px;
|
||||||
grid-template-columns: 1fr 100px;
|
grid-template-columns: 1fr 100px;
|
||||||
|
|
||||||
.mx_EventTile_line {
|
.mx_EventTile_line {
|
||||||
margin-right: 0px;
|
margin-right: 0;
|
||||||
grid-column: 1 / 3;
|
grid-column: 1 / 3;
|
||||||
padding: 0;
|
// override default padding of mx_EventTile_line so that we can be centered
|
||||||
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_EventTile_msgOption {
|
.mx_EventTile_msgOption {
|
||||||
|
|
|
@ -70,7 +70,7 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_MemberInfo_avatar {
|
.mx_MemberInfo_avatar {
|
||||||
background: $tagpanel-bg-color;
|
background: $groupFilterPanel-bg-color;
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
67
res/css/views/rooms/_NewRoomIntro.scss
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_NewRoomIntro {
|
||||||
|
margin: 40px 0 48px 64px;
|
||||||
|
|
||||||
|
.mx_MiniAvatarUploader_hasAvatar:not(.mx_MiniAvatarUploader_busy):not(:hover) {
|
||||||
|
&::before, &::after {
|
||||||
|
content: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_AccessibleButton_kind_link {
|
||||||
|
padding: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NewRoomIntro_buttons {
|
||||||
|
margin-top: 28px;
|
||||||
|
|
||||||
|
.mx_AccessibleButton {
|
||||||
|
line-height: $font-24px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: inline-block;
|
||||||
|
background-color: $button-fg-color;
|
||||||
|
mask-position: center;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
mask-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 5px;
|
||||||
|
vertical-align: text-bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_NewRoomIntro_inviteButton::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/room/invite.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
> h2 {
|
||||||
|
margin-top: 24px;
|
||||||
|
font-size: $font-24px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: $font-15px;
|
||||||
|
color: $secondary-fg-color;
|
||||||
|
}
|
||||||
|
}
|
|
@ -241,6 +241,13 @@ limitations under the License.
|
||||||
width: 26px;
|
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 {
|
.mx_RoomHeader_searchButton::before {
|
||||||
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
mask-image: url('$(res)/img/element-icons/room/search-inset.svg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ limitations under the License.
|
||||||
|
|
||||||
div:first-child {
|
div:first-child {
|
||||||
font-weight: $font-semi-bold;
|
font-weight: $font-semi-bold;
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_AccessibleButton {
|
.mx_AccessibleButton {
|
||||||
|
@ -41,6 +40,7 @@ limitations under the License.
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0 0 0 24px;
|
padding: 0 0 0 24px;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
margin-top: 8px;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
|
@ -53,6 +53,13 @@ limitations under the License.
|
||||||
mask-position: center;
|
mask-position: center;
|
||||||
mask-size: contain;
|
mask-size: contain;
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomList_explorePrompt_startChat::before {
|
||||||
|
mask-image: url('$(res)/img/element-icons/feedback.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
&.mx_RoomList_explorePrompt_explore::before {
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/explore.svg');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,10 +59,6 @@ limitations under the License.
|
||||||
width: calc(100% - 22px);
|
width: calc(100% - 22px);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.mx_RoomSublist_headerContainer_stickyBottom {
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We don't have a top style because the top is dependent on the room list header's
|
// We don't have a top style because the top is dependent on the room list header's
|
||||||
// height, and is therefore calculated in JS.
|
// height, and is therefore calculated in JS.
|
||||||
// The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
|
// The class, mx_RoomSublist_headerContainer_stickyTop, is applied though.
|
||||||
|
@ -387,3 +383,22 @@ limitations under the License.
|
||||||
.mx_RoomSublist_addRoomTooltip {
|
.mx_RoomSublist_addRoomTooltip {
|
||||||
margin-top: -3px;
|
margin-top: -3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_RoomSublist_skeletonUI {
|
||||||
|
position: relative;
|
||||||
|
margin-left: 4px;
|
||||||
|
height: 288px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
background: $roomsublist-skeleton-ui-bg;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
mask-repeat: repeat-y;
|
||||||
|
mask-size: auto 48px;
|
||||||
|
mask-image: url('$(res)/img/element-icons/roomlist/skeleton-ui.svg');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,10 @@
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx_AppTileMenuBar {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
// Sticker picker depends on the fixed height previously used for all tiles
|
// Sticker picker depends on the fixed height previously used for all tiles
|
||||||
height: 273px;
|
height: 273px;
|
||||||
|
|
|
@ -85,6 +85,7 @@ limitations under the License.
|
||||||
.mx_AvatarSetting_avatarPlaceholder {
|
.mx_AvatarSetting_avatarPlaceholder {
|
||||||
display: block;
|
display: block;
|
||||||
height: 90px;
|
height: 90px;
|
||||||
|
width: inherit;
|
||||||
border-radius: 90px;
|
border-radius: 90px;
|
||||||
cursor: pointer;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,13 +14,14 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
.mx_AnalyticsToast {
|
||||||
|
.mx_AccessibleButton_kind_danger {
|
||||||
export default function(dest, src) {
|
background: none;
|
||||||
for (const i in src) {
|
color: $accent-color;
|
||||||
if (src.hasOwnProperty(i)) {
|
}
|
||||||
dest[i] = src[i];
|
|
||||||
}
|
.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
|
pointer-events: initial; // restore pointer events so the user can leave/interact
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
.mx_VideoView {
|
.mx_CallView_video {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed {
|
.mx_VideoFeed_local {
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -92,3 +92,10 @@ limitations under the License.
|
||||||
background-color: $primary-fg-color;
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,23 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_VideoView {
|
.mx_VideoFeed video {
|
||||||
width: 100%;
|
|
||||||
position: relative;
|
|
||||||
z-index: 30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_VideoView video {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_remoteVideoFeed {
|
.mx_VideoFeed_remote {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
z-index: 50;
|
z-index: 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed {
|
.mx_VideoFeed_local {
|
||||||
width: 25%;
|
width: 25%;
|
||||||
height: 25%;
|
height: 25%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -39,11 +33,11 @@ limitations under the License.
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed video {
|
.mx_VideoFeed_local video {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_VideoView_localVideoFeed.mx_VideoView_localVideoFeed_flipped video {
|
.mx_VideoFeed_mirror video {
|
||||||
transform: scale(-1, 1);
|
transform: scale(-1, 1);
|
||||||
}
|
}
|
10
res/img/element-icons/camera.svg
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<svg width="25" height="24" viewBox="0 0 25 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.4896 2.5C9.04778 2.5 7.827 3.52171 7.54879 4.90624C7.50711 5.11367 7.42679 5.31408 7.28726 5.47312L6.6851 6.15949C6.49523 6.37591 6.22129 6.5 5.93338 6.5H2.75C1.64543 6.5 0.75 7.39543 0.75 8.5V19.5C0.75 20.6046 1.64543 21.5 2.75 21.5H22.75C23.8546 21.5 24.75 20.6046 24.75 19.5V8.5C24.75 7.39543 23.8546 6.5 22.75 6.5H19.5666C19.2787 6.5 19.0048 6.37591 18.8149 6.15949L18.2127 5.47312C18.0732 5.31408 17.9929 5.11366 17.9512 4.90623C17.673 3.5217 16.4522 2.5 15.0104 2.5H10.4896ZM16.75 13.5C16.75 15.7091 14.9591 17.5 12.75 17.5C10.5409 17.5 8.75 15.7091 8.75 13.5C8.75 11.2909 10.5409 9.5 12.75 9.5C14.9591 9.5 16.75 11.2909 16.75 13.5ZM3.25 5C2.97386 5 2.75 5.22386 2.75 5.5C2.75 5.77614 2.97386 6 3.25 6H5.25C5.52614 6 5.75 5.77614 5.75 5.5C5.75 5.22386 5.52614 5 5.25 5H3.25Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="24" height="24" fill="white" transform="translate(0.75)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
11
res/img/element-icons/chat-bubbles.svg
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.90964 11.5297C9.09231 11.5297 11.6724 8.94865 11.6724 5.76483C11.6724 2.581 9.09231 0 5.90964 0C2.72697 0 0.146904 2.581 0.146904 5.76483C0.146904 6.65678 0.3494 7.50142 0.710912 8.25525L0.0648767 10.3556C-0.171716 11.1248 0.550948 11.8442 1.31906 11.6041L3.39724 10.9544C4.15657 11.323 5.00898 11.5297 5.90964 11.5297Z" fill="black"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.03851 12.8449C5.70399 13.1151 6.4314 13.2638 7.19345 13.2638C10.3676 13.2638 13.5 10.6832 13.5 7.49979C13.5 6.63255 13.2676 5.81005 12.8651 5.07227C14.6487 6.05071 15.8583 7.94999 15.8583 10.1326C15.8583 11.0243 15.6564 11.8688 15.2959 12.6224L15.9404 14.7232C16.1765 15.4926 15.4533 16.2114 14.6854 15.9708L12.6155 15.322C11.8585 15.6902 11.0088 15.8966 10.111 15.8966C7.91459 15.8966 6.00594 14.661 5.03851 12.8449Z" fill="black"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="16" height="16" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
13
res/img/element-icons/email-prompt.svg
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<svg width="57" height="77" viewBox="0 0 57 77" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.55298 38.9352H4C1.79086 38.9352 0 40.726 0 42.9352V72.0304C0 74.2396 1.79086 76.0304 4 76.0304H53C55.2091 76.0304 57 74.2396 57 72.0304V42.9352C57 40.726 55.2091 38.9352 53 38.9352H51.365V41.6473H5.55298V38.9352ZM26.9753 61.3068L3.10141 43.4482C2.33137 42.8721 2.73876 41.6474 3.70041 41.6474H28.459H53.3841C54.3282 41.6474 54.7464 42.8352 54.0107 43.4268L31.8776 61.2212C30.4545 62.3653 28.4374 62.4005 26.9753 61.3068Z" fill="#8A8C8E"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M49.5885 33.0898C48.9384 33.2156 48.2703 33.2911 47.5885 33.3119V34.706V44.4238V54.1415H49.5885V44.4238V34.706V33.0898ZM36.5604 14.2706H13.7177C10.9562 14.2706 8.71765 16.5092 8.71765 19.2706V34.706V44.4238V54.1415H10.7177V44.4238V34.706V19.2706C10.7177 17.6138 12.0608 16.2706 13.7177 16.2706H35.5616C35.8354 15.571 36.1706 14.9022 36.5604 14.2706Z" fill="#8A8C8E"/>
|
||||||
|
<path d="M16.6589 30.5414H37.4826" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="16.2706" y1="37.8708" x2="40.6473" y2="37.8708" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="16.2706" y1="44.812" x2="40.6473" y2="44.812" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<circle cx="47.2003" cy="20.8237" r="9.71771" fill="#FE2928"/>
|
||||||
|
<rect x="45.812" y="14.5765" width="2.77649" height="8.32946" rx="1" fill="white"/>
|
||||||
|
<rect x="45.812" y="24.2943" width="2.77649" height="2.77649" rx="1" fill="white"/>
|
||||||
|
<line x1="27.3766" y1="1" x2="27.3766" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="34.3179" y1="6.55298" x2="34.3179" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<line x1="20.4354" y1="6.55298" x2="20.4354" y2="10.106" stroke="#8A8C8E" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2 KiB |
3
res/img/element-icons/feedback.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.283 21.4392C17.649 21.4392 21.9991 17.0875 21.9991 11.7196C21.9991 6.3516 17.649 2 12.283 2C6.91698 2 2.56696 6.3516 2.56696 11.7196C2.56696 13.2233 2.90831 14.6472 3.51772 15.9181L2.04565 20.7041C1.80906 21.4733 2.53172 22.1926 3.29983 21.9525L8.04605 20.4688C9.32655 21.0905 10.7641 21.4392 12.283 21.4392Z" fill="black"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 481 B |
6
res/img/element-icons/room/apps.svg
Normal file
|
@ -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">
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="20" height="20" fill="url(#paint0_linear)"/>
|
<g clip-path="url(#clip0)">
|
||||||
<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"/>
|
<rect width="20" height="20" rx="4" fill="url(#paint0_linear)"/>
|
||||||
<circle opacity="0.8" cx="10.0039" cy="10" r="7.5" stroke="white"/>
|
<path d="M2.49609 0V20" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<defs>
|
<path d="M20 2.5L1.60531e-06 2.5" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<linearGradient id="paint0_linear" x1="10" y1="0" x2="10" y2="20" gradientUnits="userSpaceOnUse">
|
<path d="M20 10L1.60531e-06 10" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<stop stop-color="#60A6FF"/>
|
<path d="M20 17.5H1.60531e-06" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
<stop offset="1" stop-color="#418DED"/>
|
<path d="M10 0.000488281V20.0005" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
</linearGradient>
|
<path d="M17.4961 0V20" stroke="white" stroke-opacity="0.5" style="mix-blend-mode:lighten"/>
|
||||||
</defs>
|
<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>
|
</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">
|
<svg width="20" height="20" viewBox="0 0 20 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 width="20" height="20" rx="4" fill="#FF4B55"/>
|
||||||
<rect x="2.96777" y="2" width="16.9843" height="5" fill="#FF4B55"/>
|
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||||
<rect x="4.96533" y="9" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
<rect x="3.96826" y="8.99951" 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"/>
|
<rect x="10.9614" y="13" width="2.99723" height="3" rx="0.25" fill="#FF4B55"/>
|
||||||
</svg>
|
</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">
|
<svg width="20" height="20" viewBox="0 0 20 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"/>
|
<rect x="1" y="1" width="18" height="18" rx="3" fill="#17191C" stroke="#17191C" stroke-width="2"/>
|
||||||
<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="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.9961 6.00049V9.81299L13.4961 11.5005" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M10 6V9.8125L12.5 11.5" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</svg>
|
</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">
|
<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"/>
|
<rect 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"/>
|
<path d="M2 7H18V16C18 17.1046 17.1046 18 16 18H4C2.89543 18 2 17.1046 2 16V7Z" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 364 B After Width: | Height: | Size: 259 B |
5
res/img/element-icons/room/default_video.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<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 |
5
res/img/element-icons/roomlist/skeleton-ui.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="228" height="48" viewBox="0 0 228 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="16" cy="16" r="16" fill="#D4D4D4"/>
|
||||||
|
<rect x="39" width="189" height="12" rx="6" fill="#D4D4D4"/>
|
||||||
|
<rect x="39" y="20" width="143" height="12" rx="6" fill="#D4D4D4"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 282 B |
5
res/img/element-icons/warning-badge.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="8" cy="8" r="8" fill="#737D8C" style="mix-blend-mode:multiply"/>
|
||||||
|
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
|
||||||
|
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 303 B |
3
res/img/feather-customised/bug.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.23858 2 7 4.23858 7 7L17 7C17 4.23858 14.7614 2 12 2ZM2.29289 7.70711C1.90237 7.31658 1.90237 6.68342 2.29289 6.29289C2.68342 5.90237 3.31658 5.90237 3.70711 6.29289L6.41421 9H17.5858L20.2929 6.29289C20.6834 5.90237 21.3166 5.90237 21.7071 6.29289C22.0976 6.68342 22.0976 7.31658 21.7071 7.70711L19 10.4142V13H22C22.5523 13 23 13.4477 23 14C23 14.5523 22.5523 15 22 15H19C19 15.7795 18.8726 16.5292 18.6375 17.2295C18.6614 17.2493 18.6847 17.2705 18.7071 17.2929L21.7071 20.2929C22.0976 20.6834 22.0976 21.3166 21.7071 21.7071C21.3166 22.0976 20.6834 22.0976 20.2929 21.7071L17.6791 19.0933C16.5924 20.5983 14.9222 21.6542 13 21.9291L13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12V21.9291C9.07785 21.6542 7.40759 20.5983 6.32091 19.0933L3.70711 21.7071C3.31658 22.0976 2.68342 22.0976 2.29289 21.7071C1.90237 21.3166 1.90237 20.6834 2.29289 20.2929L5.29289 17.2929C5.31533 17.2705 5.33857 17.2493 5.36252 17.2295C5.1274 16.5292 5 15.7795 5 15H2C1.44772 15 1 14.5523 1 14C1 13.4477 1.44772 13 2 13H5V10.4142L2.29289 7.70711Z" fill="#FF4B55"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -1,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;
|
$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;
|
$inverted-bg-color: $base-color;
|
||||||
|
|
||||||
// used by AddressSelector
|
// used by AddressSelector
|
||||||
|
@ -98,7 +98,7 @@ $roomheader-color: $text-primary-color;
|
||||||
$roomheader-bg-color: $bg-color;
|
$roomheader-bg-color: $bg-color;
|
||||||
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3);
|
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.3);
|
||||||
$roomheader-addroom-fg-color: $text-primary-color;
|
$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;
|
$groupheader-button-color: $header-panel-text-primary-color;
|
||||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||||
$icon-button-color: #8E99A4;
|
$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-bg-color: rgba(33, 38, 44, 0.90);
|
||||||
$roomlist-header-color: $tertiary-fg-color;
|
$roomlist-header-color: $tertiary-fg-color;
|
||||||
$roomsublist-divider-color: $primary-fg-color;
|
$roomsublist-divider-color: $primary-fg-color;
|
||||||
|
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||||
|
|
||||||
$tagpanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
$roomtile-preview-color: $secondary-fg-color;
|
$roomtile-preview-color: $secondary-fg-color;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
|
@ -131,6 +132,7 @@ $notice-secondary-color: $roomlist-header-color;
|
||||||
$panel-divider-color: transparent;
|
$panel-divider-color: transparent;
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
||||||
|
$widget-body-bg-color: rgba(141, 151, 165, 0.2);
|
||||||
|
|
||||||
// event tile lifecycle
|
// event tile lifecycle
|
||||||
$event-sending-color: $text-secondary-color;
|
$event-sending-color: $text-secondary-color;
|
||||||
|
@ -187,7 +189,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
||||||
|
|
||||||
$kbd-border-color: #000000;
|
$kbd-border-color: #000000;
|
||||||
|
|
||||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: $base-color;
|
$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)
|
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||||
$roomlist-background-blur-amount: 60px;
|
$roomlist-background-blur-amount: 60px;
|
||||||
$tagpanel-background-blur-amount: 30px;
|
$groupFilterPanel-background-blur-amount: 30px;
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
$composer-shadow-color: rgba(0, 0, 0, 0.28);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
@import "../../light/css/_fonts.scss";
|
@import "../../light/css/_fonts.scss";
|
||||||
@import "../../light/css/_light.scss";
|
@import "../../light/css/_light.scss";
|
||||||
// important this goes before _mods,
|
// important this goes before _mods,
|
||||||
// as $tagpanel-background-blur-amount and
|
// as $groupFilterPanel-background-blur-amount and
|
||||||
// $roomlist-background-blur-amount
|
// $roomlist-background-blur-amount
|
||||||
// are overridden in _dark.scss
|
// are overridden in _dark.scss
|
||||||
@import "_dark.scss";
|
@import "_dark.scss";
|
||||||
|
|
|
@ -37,8 +37,8 @@ $info-plinth-fg-color: #888;
|
||||||
|
|
||||||
$preview-bar-bg-color: $header-panel-bg-color;
|
$preview-bar-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$tagpanel-bg-color: $base-color;
|
$groupFilterPanel-bg-color: $base-color;
|
||||||
$inverted-bg-color: $tagpanel-bg-color;
|
$inverted-bg-color: $groupFilterPanel-bg-color;
|
||||||
|
|
||||||
// used by AddressSelector
|
// used by AddressSelector
|
||||||
$selected-color: $room-highlight-color;
|
$selected-color: $room-highlight-color;
|
||||||
|
@ -95,7 +95,7 @@ $topleftmenu-color: $text-primary-color;
|
||||||
$roomheader-color: $text-primary-color;
|
$roomheader-color: $text-primary-color;
|
||||||
$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity
|
$roomheader-addroom-bg-color: #3c4556; // $search-placeholder-color at 0.5 opacity
|
||||||
$roomheader-addroom-fg-color: $text-primary-color;
|
$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;
|
$groupheader-button-color: $header-panel-text-primary-color;
|
||||||
$rightpanel-button-color: $header-panel-text-primary-color;
|
$rightpanel-button-color: $header-panel-text-primary-color;
|
||||||
$icon-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;
|
$roomlist-bg-color: $header-panel-bg-color;
|
||||||
|
|
||||||
$roomsublist-divider-color: $primary-fg-color;
|
$roomsublist-divider-color: $primary-fg-color;
|
||||||
|
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #3e444c 0%, #3e444c00 100%);
|
||||||
|
|
||||||
$tagpanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
$roomtile-preview-color: #9e9e9e;
|
$roomtile-preview-color: #9e9e9e;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
|
@ -126,6 +127,7 @@ $roomtile-selected-bg-color: #1A1D23;
|
||||||
$panel-divider-color: $header-panel-border-color;
|
$panel-divider-color: $header-panel-border-color;
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
$widget-menu-bar-bg-color: $header-panel-bg-color;
|
||||||
|
$widget-body-bg-color: #1A1D23;
|
||||||
|
|
||||||
// event tile lifecycle
|
// event tile lifecycle
|
||||||
$event-sending-color: $text-secondary-color;
|
$event-sending-color: $text-secondary-color;
|
||||||
|
@ -182,7 +184,7 @@ $reaction-row-button-selected-border-color: $accent-color;
|
||||||
|
|
||||||
$kbd-border-color: #000000;
|
$kbd-border-color: #000000;
|
||||||
|
|
||||||
$tooltip-timeline-bg-color: $tagpanel-bg-color;
|
$tooltip-timeline-bg-color: $groupFilterPanel-bg-color;
|
||||||
$tooltip-timeline-fg-color: #ffffff;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: $base-color;
|
$interactive-tooltip-bg-color: $base-color;
|
||||||
|
|
|
@ -67,8 +67,8 @@ $preview-bar-bg-color: #f7f7f7;
|
||||||
$secondary-accent-color: #f2f5f8;
|
$secondary-accent-color: #f2f5f8;
|
||||||
$tertiary-accent-color: #d3efe1;
|
$tertiary-accent-color: #d3efe1;
|
||||||
|
|
||||||
$tagpanel-bg-color: #27303a;
|
$groupFilterPanel-bg-color: #27303a;
|
||||||
$inverted-bg-color: $tagpanel-bg-color;
|
$inverted-bg-color: $groupFilterPanel-bg-color;
|
||||||
|
|
||||||
// used by RoomDirectory permissions
|
// used by RoomDirectory permissions
|
||||||
$plinth-bg-color: $secondary-accent-color;
|
$plinth-bg-color: $secondary-accent-color;
|
||||||
|
@ -162,7 +162,7 @@ $roomheader-color: #45474a;
|
||||||
$roomheader-bg-color: $primary-bg-color;
|
$roomheader-bg-color: $primary-bg-color;
|
||||||
$roomheader-addroom-bg-color: #91a1c0;
|
$roomheader-addroom-bg-color: #91a1c0;
|
||||||
$roomheader-addroom-fg-color: $accent-fg-color;
|
$roomheader-addroom-fg-color: $accent-fg-color;
|
||||||
$tagpanel-button-color: #91a1c0;
|
$groupFilterPanel-button-color: #91a1c0;
|
||||||
$groupheader-button-color: #91a1c0;
|
$groupheader-button-color: #91a1c0;
|
||||||
$rightpanel-button-color: #91a1c0;
|
$rightpanel-button-color: #91a1c0;
|
||||||
$icon-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-bg-color: $header-panel-bg-color;
|
||||||
$roomlist-header-color: $primary-fg-color;
|
$roomlist-header-color: $primary-fg-color;
|
||||||
$roomsublist-divider-color: $primary-fg-color;
|
$roomsublist-divider-color: $primary-fg-color;
|
||||||
|
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
|
||||||
|
|
||||||
$tagpanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
$roomtile-preview-color: #9e9e9e;
|
$roomtile-preview-color: #9e9e9e;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
|
@ -208,6 +209,7 @@ $panel-divider-color: #dee1f3;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $secondary-accent-color;
|
$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;
|
$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;
|
$tooltip-timeline-fg-color: #ffffff;
|
||||||
|
|
||||||
$interactive-tooltip-bg-color: #27303a;
|
$interactive-tooltip-bg-color: #27303a;
|
||||||
|
|
|
@ -49,7 +49,7 @@ $roomtile-selected-bg-color: var(--roomlist-highlights-color);
|
||||||
//
|
//
|
||||||
// --sidebar-color
|
// --sidebar-color
|
||||||
$interactive-tooltip-bg-color: var(--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);
|
$tooltip-timeline-bg-color: var(--sidebar-color);
|
||||||
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
$dialog-backdrop-color: var(--sidebar-color-50pct);
|
||||||
$roomlist-button-bg-color: var(--sidebar-color-15pct);
|
$roomlist-button-bg-color: var(--sidebar-color-15pct);
|
||||||
|
|
|
@ -62,7 +62,7 @@ $preview-bar-bg-color: #f7f7f7;
|
||||||
$secondary-accent-color: #f2f5f8;
|
$secondary-accent-color: #f2f5f8;
|
||||||
$tertiary-accent-color: #d3efe1;
|
$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
|
// used by RoomDirectory permissions
|
||||||
$plinth-bg-color: $secondary-accent-color;
|
$plinth-bg-color: $secondary-accent-color;
|
||||||
|
@ -156,7 +156,7 @@ $roomheader-color: #45474a;
|
||||||
$roomheader-bg-color: $primary-bg-color;
|
$roomheader-bg-color: $primary-bg-color;
|
||||||
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2);
|
$roomheader-addroom-bg-color: rgba(92, 100, 112, 0.2);
|
||||||
$roomheader-addroom-fg-color: #5c6470;
|
$roomheader-addroom-fg-color: #5c6470;
|
||||||
$tagpanel-button-color: #91A1C0;
|
$groupFilterPanel-button-color: #91A1C0;
|
||||||
$groupheader-button-color: #91A1C0;
|
$groupheader-button-color: #91A1C0;
|
||||||
$rightpanel-button-color: #91A1C0;
|
$rightpanel-button-color: #91A1C0;
|
||||||
$icon-button-color: #C1C6CD;
|
$icon-button-color: #C1C6CD;
|
||||||
|
@ -175,8 +175,9 @@ $roomlist-filter-active-bg-color: #ffffff;
|
||||||
$roomlist-bg-color: rgba(245, 245, 245, 0.90);
|
$roomlist-bg-color: rgba(245, 245, 245, 0.90);
|
||||||
$roomlist-header-color: $tertiary-fg-color;
|
$roomlist-header-color: $tertiary-fg-color;
|
||||||
$roomsublist-divider-color: $primary-fg-color;
|
$roomsublist-divider-color: $primary-fg-color;
|
||||||
|
$roomsublist-skeleton-ui-bg: linear-gradient(180deg, #ffffff 0%, #ffffff00 100%);
|
||||||
|
|
||||||
$tagpanel-divider-color: $roomlist-header-color;
|
$groupFilterPanel-divider-color: $roomlist-header-color;
|
||||||
|
|
||||||
$roomtile-preview-color: $secondary-fg-color;
|
$roomtile-preview-color: $secondary-fg-color;
|
||||||
$roomtile-default-badge-bg-color: #61708b;
|
$roomtile-default-badge-bg-color: #61708b;
|
||||||
|
@ -208,6 +209,7 @@ $pinned-color: $notice-secondary-color;
|
||||||
// ********************
|
// ********************
|
||||||
|
|
||||||
$widget-menu-bar-bg-color: $secondary-accent-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)
|
// blur amounts for left left panel (only for element theme, used in _mods.scss)
|
||||||
$roomlist-background-blur-amount: 40px;
|
$roomlist-background-blur-amount: 40px;
|
||||||
$tagpanel-background-blur-amount: 20px;
|
$groupFilterPanel-background-blur-amount: 20px;
|
||||||
|
|
||||||
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
$composer-shadow-color: rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
|
|
|
@ -6,14 +6,14 @@
|
||||||
|
|
||||||
@supports (backdrop-filter: none) {
|
@supports (backdrop-filter: none) {
|
||||||
.mx_LeftPanel {
|
.mx_LeftPanel {
|
||||||
background-image: var(--avatar-url);
|
background-image: var(--avatar-url, unset);
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: left top;
|
background-position: left top;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_TagPanel {
|
.mx_GroupFilterPanel {
|
||||||
backdrop-filter: blur($tagpanel-background-blur-amount);
|
backdrop-filter: blur($groupFilterPanel-background-blur-amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
.mx_LeftPanel .mx_LeftPanel_roomListContainer {
|
||||||
|
|
31
src/@types/global.d.ts
vendored
|
@ -32,6 +32,10 @@ import type {Renderer} from "react-dom";
|
||||||
import RightPanelStore from "../stores/RightPanelStore";
|
import RightPanelStore from "../stores/RightPanelStore";
|
||||||
import WidgetStore from "../stores/WidgetStore";
|
import WidgetStore from "../stores/WidgetStore";
|
||||||
import CallHandler from "../CallHandler";
|
import CallHandler from "../CallHandler";
|
||||||
|
import {Analytics} from "../Analytics";
|
||||||
|
import CountlyAnalytics from "../CountlyAnalytics";
|
||||||
|
import UserActivity from "../UserActivity";
|
||||||
|
import {ModalWidgetStore} from "../stores/ModalWidgetStore";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -56,11 +60,22 @@ declare global {
|
||||||
mxRightPanelStore: RightPanelStore;
|
mxRightPanelStore: RightPanelStore;
|
||||||
mxWidgetStore: WidgetStore;
|
mxWidgetStore: WidgetStore;
|
||||||
mxCallHandler: CallHandler;
|
mxCallHandler: CallHandler;
|
||||||
|
mxAnalytics: Analytics;
|
||||||
|
mxCountlyAnalytics: typeof CountlyAnalytics;
|
||||||
|
mxUserActivity: UserActivity;
|
||||||
|
mxModalWidgetStore: ModalWidgetStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Document {
|
interface Document {
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
|
||||||
hasStorageAccess?: () => Promise<boolean>;
|
hasStorageAccess?: () => Promise<boolean>;
|
||||||
|
|
||||||
|
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||||
|
// previously so let's continue to support them for now
|
||||||
|
webkitExitFullscreen(): Promise<void>;
|
||||||
|
msExitFullscreen(): Promise<void>;
|
||||||
|
readonly webkitFullscreenElement: Element | null;
|
||||||
|
readonly msFullscreenElement: Element | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Navigator {
|
interface Navigator {
|
||||||
|
@ -90,4 +105,20 @@ declare global {
|
||||||
interface HTMLAudioElement {
|
interface HTMLAudioElement {
|
||||||
type?: string;
|
type?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Element {
|
||||||
|
// Safari & IE11 only have this prefixed: we used prefixed versions
|
||||||
|
// previously so let's continue to support them for now
|
||||||
|
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Error {
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/fileName
|
||||||
|
fileName?: string;
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/lineNumber
|
||||||
|
lineNumber?: number;
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/columnNumber
|
||||||
|
columnNumber?: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { getCurrentLanguage, _t, _td } from './languageHandler';
|
import {getCurrentLanguage, _t, _td, IVariables} from './languageHandler';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import SdkConfig from './SdkConfig';
|
import SdkConfig from './SdkConfig';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
@ -27,7 +27,7 @@ const hashRegex = /#\/(groups?|room|user|settings|register|login|forgot_password
|
||||||
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
const hashVarRegex = /#\/(group|room|user)\/.*$/;
|
||||||
|
|
||||||
// Remove all but the first item in the hash path. Redact unexpected hashes.
|
// 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
|
// Don't leak URLs we aren't expecting - they could contain tokens/PII
|
||||||
const match = hashRegex.exec(hash);
|
const match = hashRegex.exec(hash);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
|
@ -44,7 +44,7 @@ function getRedactedHash(hash) {
|
||||||
|
|
||||||
// Return the current origin, path and hash separated with a `/`. This does
|
// Return the current origin, path and hash separated with a `/`. This does
|
||||||
// not include query parameters.
|
// not include query parameters.
|
||||||
function getRedactedUrl() {
|
function getRedactedUrl(): string {
|
||||||
const { origin, hash } = window.location;
|
const { origin, hash } = window.location;
|
||||||
let { pathname } = window.location;
|
let { pathname } = window.location;
|
||||||
|
|
||||||
|
@ -56,7 +56,25 @@ function getRedactedUrl() {
|
||||||
return origin + pathname + getRedactedHash(hash);
|
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
|
// The Matomo installation at https://matomo.riot.im is currently configured
|
||||||
// with a limit of 10 custom variables.
|
// with a limit of 10 custom variables.
|
||||||
'App Platform': {
|
'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;
|
if (whitelist.includes(str)) return str;
|
||||||
return '<redacted>';
|
return '<redacted>';
|
||||||
}
|
}
|
||||||
|
@ -130,7 +148,7 @@ const CREATION_TS_KEY = "mx_Riot_Analytics_cts";
|
||||||
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
|
const VISIT_COUNT_KEY = "mx_Riot_Analytics_vc";
|
||||||
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
|
const LAST_VISIT_TS_KEY = "mx_Riot_Analytics_lvts";
|
||||||
|
|
||||||
function getUid() {
|
function getUid(): string {
|
||||||
try {
|
try {
|
||||||
let data = localStorage && localStorage.getItem(UID_KEY);
|
let data = localStorage && localStorage.getItem(UID_KEY);
|
||||||
if (!data && localStorage) {
|
if (!data && localStorage) {
|
||||||
|
@ -145,32 +163,36 @@ function getUid() {
|
||||||
|
|
||||||
const HEARTBEAT_INTERVAL = 30 * 1000; // seconds
|
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() {
|
constructor() {
|
||||||
this.baseUrl = null;
|
|
||||||
this.siteId = null;
|
|
||||||
this.visitVariables = {};
|
|
||||||
|
|
||||||
this.firstPage = true;
|
|
||||||
this._heartbeatIntervalID = null;
|
|
||||||
|
|
||||||
this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY);
|
this.creationTs = localStorage && localStorage.getItem(CREATION_TS_KEY);
|
||||||
if (!this.creationTs && localStorage) {
|
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.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) {
|
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;
|
return !this.baseUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
canEnable() {
|
public canEnable() {
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
return navigator.doNotTrack !== "1" && config && config.piwik && config.piwik.url && config.piwik.siteId;
|
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
|
* Enable Analytics if initialized but disabled
|
||||||
* otherwise try and initalize, no-op if piwik config missing
|
* otherwise try and initalize, no-op if piwik config missing
|
||||||
*/
|
*/
|
||||||
async enable() {
|
public async enable() {
|
||||||
if (!this.disabled) return;
|
if (!this.disabled) return;
|
||||||
if (!this.canEnable()) return;
|
if (!this.canEnable()) return;
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
|
|
||||||
this.baseUrl = new URL("piwik.php", config.piwik.url);
|
this.baseUrl = new URL("piwik.php", config.piwik.url);
|
||||||
// set constants
|
// 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("idsite", config.piwik.siteId); // rec is required for tracking
|
||||||
this.baseUrl.searchParams.set("apiv", 1); // API version to use
|
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("send_image", "0"); // we want a 204, not a tiny GIF
|
||||||
// set user parameters
|
// set user parameters
|
||||||
this.baseUrl.searchParams.set("_id", getUid()); // uuid
|
this.baseUrl.searchParams.set("_id", getUid()); // uuid
|
||||||
this.baseUrl.searchParams.set("_idts", this.creationTs); // first ts
|
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) {
|
if (this.lastVisitTs) {
|
||||||
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
|
this.baseUrl.searchParams.set("_viewts", this.lastVisitTs); // last visit ts
|
||||||
}
|
}
|
||||||
|
|
||||||
const platform = PlatformPeg.get();
|
const platform = PlatformPeg.get();
|
||||||
this._setVisitVariable('App Platform', platform.getHumanReadableName());
|
this.setVisitVariable('App Platform', platform.getHumanReadableName());
|
||||||
try {
|
try {
|
||||||
this._setVisitVariable('App Version', await platform.getAppVersion());
|
this.setVisitVariable('App Version', await platform.getAppVersion());
|
||||||
} catch (e) {
|
} 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;
|
const hostname = window.location.hostname;
|
||||||
if (hostname === 'riot.im') {
|
if (hostname === 'riot.im') {
|
||||||
this._setVisitVariable('Instance', window.location.pathname);
|
this.setVisitVariable('Instance', window.location.pathname);
|
||||||
} else if (hostname.endsWith('.element.io')) {
|
} else if (hostname.endsWith('.element.io')) {
|
||||||
this._setVisitVariable('Instance', hostname.replace('.element.io', ''));
|
this.setVisitVariable('Instance', hostname.replace('.element.io', ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
let installedPWA = "unknown";
|
let installedPWA = "unknown";
|
||||||
try {
|
try {
|
||||||
// Known to work at least for desktop Chrome
|
// 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) { }
|
} catch (e) { }
|
||||||
this._setVisitVariable('Installed PWA', installedPWA);
|
this.setVisitVariable('Installed PWA', installedPWA);
|
||||||
|
|
||||||
let touchInput = "unknown";
|
let touchInput = "unknown";
|
||||||
try {
|
try {
|
||||||
// MDN claims broad support across browsers
|
// MDN claims broad support across browsers
|
||||||
touchInput = window.matchMedia('(pointer: coarse)').matches;
|
touchInput = String(window.matchMedia('(pointer: coarse)').matches);
|
||||||
} catch (e) { }
|
} catch (e) { }
|
||||||
this._setVisitVariable('Touch Input', touchInput);
|
this.setVisitVariable('Touch Input', touchInput);
|
||||||
|
|
||||||
// start heartbeat
|
// 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 Analytics, stop the heartbeat and clear identifiers from localStorage
|
||||||
*/
|
*/
|
||||||
disable() {
|
public disable() {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.trackEvent('Analytics', 'opt-out');
|
this.trackEvent('Analytics', 'opt-out');
|
||||||
window.clearInterval(this._heartbeatIntervalID);
|
window.clearInterval(this.heartbeatIntervalID);
|
||||||
this.baseUrl = null;
|
this.baseUrl = null;
|
||||||
this.visitVariables = {};
|
this.visitVariables = {};
|
||||||
localStorage.removeItem(UID_KEY);
|
localStorage.removeItem(UID_KEY);
|
||||||
|
@ -248,7 +270,7 @@ class Analytics {
|
||||||
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
localStorage.removeItem(LAST_VISIT_TS_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
async _track(data) {
|
private async _track(data: IData) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
@ -264,13 +286,13 @@ class Analytics {
|
||||||
s: now.getSeconds(),
|
s: now.getSeconds(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = new URL(this.baseUrl);
|
const url = new URL(this.baseUrl.toString()); // copy
|
||||||
for (const key in params) {
|
for (const key in params) {
|
||||||
url.searchParams.set(key, params[key]);
|
url.searchParams.set(key, params[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await window.fetch(url, {
|
await window.fetch(url.toString(), {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
mode: "no-cors",
|
mode: "no-cors",
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
|
@ -281,14 +303,14 @@ class Analytics {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ping() {
|
public ping() {
|
||||||
this._track({
|
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.disabled) return;
|
||||||
if (this.firstPage) {
|
if (this.firstPage) {
|
||||||
// De-duplicate first page
|
// De-duplicate first page
|
||||||
|
@ -303,11 +325,11 @@ class Analytics {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._track({
|
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;
|
if (this.disabled) return;
|
||||||
this._track({
|
this._track({
|
||||||
e_c: category,
|
e_c: category,
|
||||||
|
@ -317,12 +339,12 @@ class Analytics {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_setVisitVariable(key, value) {
|
private setVisitVariable(key: keyof typeof customVariables, value: string) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this.visitVariables[customVariables[key].id] = [key, value];
|
this.visitVariables[customVariables[key].id] = [key, value];
|
||||||
}
|
}
|
||||||
|
|
||||||
setLoggedIn(isGuest, homeserverUrl, identityServerUrl) {
|
public setLoggedIn(isGuest: boolean, homeserverUrl: string) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
|
|
||||||
const config = SdkConfig.get();
|
const config = SdkConfig.get();
|
||||||
|
@ -330,16 +352,16 @@ class Analytics {
|
||||||
|
|
||||||
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
const whitelistedHSUrls = config.piwik.whitelistedHSUrls || [];
|
||||||
|
|
||||||
this._setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
this.setVisitVariable('User Type', isGuest ? 'Guest' : 'Logged In');
|
||||||
this._setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
this.setVisitVariable('Homeserver URL', whitelistRedact(whitelistedHSUrls, homeserverUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
setBreadcrumbs(state) {
|
public setBreadcrumbs(state: boolean) {
|
||||||
if (this.disabled) return;
|
if (this.disabled) return;
|
||||||
this._setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
this.setVisitVariable('Breadcrumbs', state ? 'enabled' : 'disabled');
|
||||||
}
|
}
|
||||||
|
|
||||||
showDetailsModal = () => {
|
public showDetailsModal = () => {
|
||||||
let rows = [];
|
let rows = [];
|
||||||
if (!this.disabled) {
|
if (!this.disabled) {
|
||||||
rows = Object.values(this.visitVariables);
|
rows = Object.values(this.visitVariables);
|
||||||
|
@ -360,7 +382,7 @@ class Analytics {
|
||||||
'e.g. <CurrentPageURL>',
|
'e.g. <CurrentPageURL>',
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
CurrentPageURL: getRedactedUrl(),
|
CurrentPageURL: getRedactedUrl,
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
@ -401,7 +423,7 @@ class Analytics {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!global.mxAnalytics) {
|
if (!window.mxAnalytics) {
|
||||||
global.mxAnalytics = new Analytics();
|
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.
|
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 {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import DMRoomMap from './utils/DMRoomMap';
|
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
|
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already
|
||||||
export function avatarUrlForMember(member, width, height, resizeMethod) {
|
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
|
||||||
let url;
|
let url: string;
|
||||||
if (member && member.getAvatarUrl) {
|
if (member && member.getAvatarUrl) {
|
||||||
url = member.getAvatarUrl(
|
url = member.getAvatarUrl(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(),
|
MatrixClientPeg.get().getHomeserverUrl(),
|
||||||
|
@ -41,7 +46,7 @@ export function avatarUrlForMember(member, width, height, resizeMethod) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function avatarUrlForUser(user, width, height, resizeMethod) {
|
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
|
||||||
const url = getHttpUriForMxc(
|
const url = getHttpUriForMxc(
|
||||||
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
MatrixClientPeg.get().getHomeserverUrl(), user.avatarUrl,
|
||||||
Math.floor(width * window.devicePixelRatio),
|
Math.floor(width * window.devicePixelRatio),
|
||||||
|
@ -54,14 +59,14 @@ export function avatarUrlForUser(user, width, height, resizeMethod) {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidHexColor(color) {
|
function isValidHexColor(color: string): boolean {
|
||||||
return typeof color === "string" &&
|
return typeof color === "string" &&
|
||||||
(color.length === 7 || color.lengh === 9) &&
|
(color.length === 7 || color.length === 9) &&
|
||||||
color.charAt(0) === "#" &&
|
color.charAt(0) === "#" &&
|
||||||
!color.substr(1).split("").some(c => isNaN(parseInt(c, 16)));
|
!color.substr(1).split("").some(c => isNaN(parseInt(c, 16)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function urlForColor(color) {
|
function urlForColor(color: string): string {
|
||||||
const size = 40;
|
const size = 40;
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
canvas.width = size;
|
canvas.width = size;
|
||||||
|
@ -79,9 +84,9 @@ function urlForColor(color) {
|
||||||
// XXX: Ideally we'd clear this cache when the theme changes
|
// XXX: Ideally we'd clear this cache when the theme changes
|
||||||
// but since this function is at global scope, it's a bit
|
// 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
|
// 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
|
if (!s) return ""; // XXX: should never happen but empirically does by evidence of a rageshake
|
||||||
const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
|
const defaultColors = ['#0DBD8B', '#368bd6', '#ac3ba8'];
|
||||||
let total = 0;
|
let total = 0;
|
||||||
|
@ -113,7 +118,7 @@ export function defaultAvatarUrlForString(s) {
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @return {string} the first letter
|
* @return {string} the first letter
|
||||||
*/
|
*/
|
||||||
export function getInitialLetter(name) {
|
export function getInitialLetter(name: string): string {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
// XXX: We should find out what causes the name to sometimes be falsy.
|
// XXX: We should find out what causes the name to sometimes be falsy.
|
||||||
console.trace("`name` argument to `getInitialLetter` not supplied");
|
console.trace("`name` argument to `getInitialLetter` not supplied");
|
||||||
|
@ -146,7 +151,7 @@ export function getInitialLetter(name) {
|
||||||
return firstChar.toUpperCase();
|
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
|
if (!room) return null; // null-guard
|
||||||
|
|
||||||
const explicitRoomAvatar = room.getAvatarUrl(
|
const explicitRoomAvatar = room.getAvatarUrl(
|
|
@ -24,6 +24,7 @@ import {ActionPayload} from "./dispatcher/payloads";
|
||||||
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
import {CheckUpdatesPayload} from "./dispatcher/payloads/CheckUpdatesPayload";
|
||||||
import {Action} from "./dispatcher/actions";
|
import {Action} from "./dispatcher/actions";
|
||||||
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
import {hideToast as hideUpdateToast} from "./toasts/UpdateToast";
|
||||||
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
|
|
||||||
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
export const SSO_HOMESERVER_URL_KEY = "mx_sso_hs_url";
|
||||||
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
export const SSO_ID_SERVER_URL_KEY = "mx_sso_is_url";
|
||||||
|
@ -105,6 +106,9 @@ export default abstract class BasePlatform {
|
||||||
* @param newVersion the version string to check
|
* @param newVersion the version string to check
|
||||||
*/
|
*/
|
||||||
protected shouldShowUpdate(newVersion: string): boolean {
|
protected shouldShowUpdate(newVersion: string): boolean {
|
||||||
|
// If the user registered on this client in the last 24 hours then do not show them the update toast
|
||||||
|
if (MatrixClientPeg.userRegisteredWithinLastHours(24)) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
|
const [version, deferUntil] = JSON.parse(localStorage.getItem(UPDATE_DEFER_KEY));
|
||||||
return newVersion !== version || Date.now() > deferUntil;
|
return newVersion !== version || Date.now() > deferUntil;
|
||||||
|
|
|
@ -59,8 +59,7 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import PlatformPeg from './PlatformPeg';
|
import PlatformPeg from './PlatformPeg';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
// @ts-ignore - XXX: tsc doesn't like this: our js-sdk imports are complex so this isn't surprising
|
import { createNewMatrixCall } from 'matrix-js-sdk/src/webrtc/call';
|
||||||
import Matrix from 'matrix-js-sdk';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import WidgetUtils from './utils/WidgetUtils';
|
import WidgetUtils from './utils/WidgetUtils';
|
||||||
import WidgetEchoStore from './stores/WidgetEchoStore';
|
import WidgetEchoStore from './stores/WidgetEchoStore';
|
||||||
|
@ -77,7 +76,9 @@ import ErrorDialog from "./components/views/dialogs/ErrorDialog";
|
||||||
import WidgetStore from "./stores/WidgetStore";
|
import WidgetStore from "./stores/WidgetStore";
|
||||||
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
|
||||||
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
|
||||||
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty } from "matrix-js-sdk/lib/webrtc/call";
|
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
|
||||||
|
import Analytics from './Analytics';
|
||||||
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
|
|
||||||
enum AudioID {
|
enum AudioID {
|
||||||
Ring = 'ringAudio',
|
Ring = 'ringAudio',
|
||||||
|
@ -96,6 +97,21 @@ export enum PlaceCallType {
|
||||||
ScreenSharing = 'screensharing',
|
ScreenSharing = 'screensharing',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRemoteAudioElement(): HTMLAudioElement {
|
||||||
|
// this needs to be somewhere at the top of the DOM which
|
||||||
|
// always exists to avoid audio interruptions.
|
||||||
|
// Might as well just use DOM.
|
||||||
|
const remoteAudioElement = document.getElementById("remoteAudio") as HTMLAudioElement;
|
||||||
|
if (!remoteAudioElement) {
|
||||||
|
console.error(
|
||||||
|
"Failed to find remoteAudio element - cannot play audio!" +
|
||||||
|
"You need to add an <audio/> to the DOM.",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return remoteAudioElement;
|
||||||
|
}
|
||||||
|
|
||||||
export default class CallHandler {
|
export default class CallHandler {
|
||||||
private calls = new Map<string, MatrixCall>();
|
private calls = new Map<string, MatrixCall>();
|
||||||
private audioPromises = new Map<AudioID, Promise<void>>();
|
private audioPromises = new Map<AudioID, Promise<void>>();
|
||||||
|
@ -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) {
|
private setCallListeners(call: MatrixCall) {
|
||||||
call.on(CallEvent.Error, (err) => {
|
call.on(CallEvent.Error, (err) => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
Analytics.trackEvent('voip', 'callError', 'error', err);
|
||||||
console.error("Call error:", err);
|
console.error("Call error:", err);
|
||||||
if (
|
if (
|
||||||
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
MatrixClientPeg.get().getTurnServers().length === 0 &&
|
||||||
|
@ -196,9 +223,15 @@ export default class CallHandler {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
call.on(CallEvent.Hangup, () => {
|
call.on(CallEvent.Hangup, () => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
|
Analytics.trackEvent('voip', 'callHangup');
|
||||||
|
|
||||||
this.removeCallForRoom(call.roomId);
|
this.removeCallForRoom(call.roomId);
|
||||||
});
|
});
|
||||||
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
call.on(CallEvent.State, (newState: CallState, oldState: CallState) => {
|
||||||
|
if (!this.matchesCallForThisRoom(call)) return;
|
||||||
|
|
||||||
this.setCallState(call, newState);
|
this.setCallState(call, newState);
|
||||||
|
|
||||||
switch (oldState) {
|
switch (oldState) {
|
||||||
|
@ -218,21 +251,63 @@ export default class CallHandler {
|
||||||
this.play(AudioID.Ringback);
|
this.play(AudioID.Ringback);
|
||||||
break;
|
break;
|
||||||
case CallState.Ended:
|
case CallState.Ended:
|
||||||
|
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
|
||||||
this.removeCallForRoom(call.roomId);
|
this.removeCallForRoom(call.roomId);
|
||||||
if (oldState === CallState.InviteSent && (
|
if (oldState === CallState.InviteSent && (
|
||||||
call.hangupParty === CallParty.Remote ||
|
call.hangupParty === CallParty.Remote ||
|
||||||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
|
||||||
)) {
|
)) {
|
||||||
this.play(AudioID.Busy);
|
this.play(AudioID.Busy);
|
||||||
Modal.createTrackedDialog('Call Handler', 'Call Timeout', ErrorDialog, {
|
let title;
|
||||||
title: _t('Call Timeout'),
|
let description;
|
||||||
description: _t('The remote side failed to pick up') + '.',
|
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 {
|
} else {
|
||||||
this.play(AudioID.CallEnd);
|
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) {
|
private setCallState(call: MatrixCall, status: CallState) {
|
||||||
|
@ -285,9 +360,13 @@ export default class CallHandler {
|
||||||
roomId: string, type: PlaceCallType,
|
roomId: string, type: PlaceCallType,
|
||||||
localElement: HTMLVideoElement, remoteElement: HTMLVideoElement,
|
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.calls.set(roomId, call);
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
this.setCallAudioElement(call);
|
||||||
|
|
||||||
if (type === PlaceCallType.Voice) {
|
if (type === PlaceCallType.Voice) {
|
||||||
call.placeVoiceCall();
|
call.placeVoiceCall();
|
||||||
} else if (type === 'video') {
|
} else if (type === 'video') {
|
||||||
|
@ -362,6 +441,8 @@ export default class CallHandler {
|
||||||
break;
|
break;
|
||||||
case 'place_conference_call':
|
case 'place_conference_call':
|
||||||
console.info("Place conference call in %s", payload.room_id);
|
console.info("Place conference call in %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);
|
this.startCallApp(payload.room_id, payload.type);
|
||||||
break;
|
break;
|
||||||
case 'end_conference':
|
case 'end_conference':
|
||||||
|
@ -388,27 +469,37 @@ export default class CallHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
const call = payload.call as MatrixCall;
|
const call = payload.call as MatrixCall;
|
||||||
|
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
|
||||||
this.calls.set(call.roomId, call)
|
this.calls.set(call.roomId, call)
|
||||||
this.setCallListeners(call);
|
this.setCallListeners(call);
|
||||||
|
this.setCallAudioElement(call);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'hangup':
|
case 'hangup':
|
||||||
|
case 'reject':
|
||||||
if (!this.calls.get(payload.room_id)) {
|
if (!this.calls.get(payload.room_id)) {
|
||||||
return; // no call to hangup
|
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);
|
this.removeCallForRoom(payload.room_id);
|
||||||
break;
|
break;
|
||||||
case 'answer':
|
case 'answer': {
|
||||||
if (!this.calls.has(payload.room_id)) {
|
if (!this.calls.has(payload.room_id)) {
|
||||||
return; // no call to answer
|
return; // no call to answer
|
||||||
}
|
}
|
||||||
this.calls.get(payload.room_id).answer();
|
const call = this.calls.get(payload.room_id);
|
||||||
|
call.answer();
|
||||||
|
CountlyAnalytics.instance.trackJoinCall(payload.room_id, call.type === CallType.Video, false);
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: "view_room",
|
action: "view_room",
|
||||||
room_id: payload.room_id,
|
room_id: payload.room_id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import extend from './extend';
|
|
||||||
import dis from './dispatcher/dispatcher';
|
import dis from './dispatcher/dispatcher';
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
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
|
// Polyfill for Canvas.toBlob API using Canvas.toDataURL
|
||||||
import "blueimp-canvas-to-blob";
|
import "blueimp-canvas-to-blob";
|
||||||
import { Action } from "./dispatcher/actions";
|
import { Action } from "./dispatcher/actions";
|
||||||
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
|
|
||||||
const MAX_WIDTH = 800;
|
const MAX_WIDTH = 800;
|
||||||
const MAX_HEIGHT = 600;
|
const MAX_HEIGHT = 600;
|
||||||
|
@ -369,10 +369,13 @@ export default class ContentMessages {
|
||||||
private mediaConfig: IMediaConfig = null;
|
private mediaConfig: IMediaConfig = null;
|
||||||
|
|
||||||
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
|
sendStickerContentToRoom(url: string, roomId: string, info: string, text: string, matrixClient: MatrixClient) {
|
||||||
return MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
|
const prom = MatrixClientPeg.get().sendStickerMessage(roomId, url, info, text).catch((e) => {
|
||||||
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
console.warn(`Failed to send content with URL ${url} to room ${roomId}`, e);
|
||||||
throw e;
|
throw e;
|
||||||
});
|
});
|
||||||
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, {msgtype: "m.sticker"});
|
||||||
|
return prom;
|
||||||
}
|
}
|
||||||
|
|
||||||
getUploadLimit() {
|
getUploadLimit() {
|
||||||
|
@ -480,6 +483,7 @@ export default class ContentMessages {
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
|
private sendContentToRoom(file: File, roomId: string, matrixClient: MatrixClient, promBefore: Promise<any>) {
|
||||||
|
const startTime = CountlyAnalytics.getTimestamp();
|
||||||
const content: IContent = {
|
const content: IContent = {
|
||||||
body: file.name || 'Attachment',
|
body: file.name || 'Attachment',
|
||||||
info: {
|
info: {
|
||||||
|
@ -497,7 +501,7 @@ export default class ContentMessages {
|
||||||
if (file.type.indexOf('image/') === 0) {
|
if (file.type.indexOf('image/') === 0) {
|
||||||
content.msgtype = 'm.image';
|
content.msgtype = 'm.image';
|
||||||
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
infoForImageFile(matrixClient, roomId, file).then((imageInfo) => {
|
||||||
extend(content.info, imageInfo);
|
Object.assign(content.info, imageInfo);
|
||||||
resolve();
|
resolve();
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
@ -510,7 +514,7 @@ export default class ContentMessages {
|
||||||
} else if (file.type.indexOf('video/') === 0) {
|
} else if (file.type.indexOf('video/') === 0) {
|
||||||
content.msgtype = 'm.video';
|
content.msgtype = 'm.video';
|
||||||
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
|
infoForVideoFile(matrixClient, roomId, file).then((videoInfo) => {
|
||||||
extend(content.info, videoInfo);
|
Object.assign(content.info, videoInfo);
|
||||||
resolve();
|
resolve();
|
||||||
}, (e) => {
|
}, (e) => {
|
||||||
content.msgtype = 'm.file';
|
content.msgtype = 'm.file';
|
||||||
|
@ -564,7 +568,9 @@ export default class ContentMessages {
|
||||||
return promBefore;
|
return promBefore;
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
if (upload.canceled) throw new UploadCanceledError();
|
if (upload.canceled) throw new UploadCanceledError();
|
||||||
return matrixClient.sendMessage(roomId, content);
|
const prom = matrixClient.sendMessage(roomId, content);
|
||||||
|
CountlyAnalytics.instance.trackSendMessage(startTime, prom, roomId, false, false, content);
|
||||||
|
return prom;
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
error = err;
|
error = err;
|
||||||
if (!upload.canceled) {
|
if (!upload.canceled) {
|
||||||
|
|
973
src/CountlyAnalytics.ts
Normal file
|
@ -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';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
function getDaysArray() {
|
function getDaysArray(): string[] {
|
||||||
return [
|
return [
|
||||||
_t('Sun'),
|
_t('Sun'),
|
||||||
_t('Mon'),
|
_t('Mon'),
|
||||||
|
@ -29,7 +29,7 @@ function getDaysArray() {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getMonthsArray() {
|
function getMonthsArray(): string[] {
|
||||||
return [
|
return [
|
||||||
_t('Jan'),
|
_t('Jan'),
|
||||||
_t('Feb'),
|
_t('Feb'),
|
||||||
|
@ -46,11 +46,11 @@ function getMonthsArray() {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function pad(n) {
|
function pad(n: number): string {
|
||||||
return (n < 10 ? '0' : '') + n;
|
return (n < 10 ? '0' : '') + n;
|
||||||
}
|
}
|
||||||
|
|
||||||
function twelveHourTime(date, showSeconds=false) {
|
function twelveHourTime(date: Date, showSeconds = false): string {
|
||||||
let hours = date.getHours() % 12;
|
let hours = date.getHours() % 12;
|
||||||
const minutes = pad(date.getMinutes());
|
const minutes = pad(date.getMinutes());
|
||||||
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
const ampm = date.getHours() >= 12 ? _t('PM') : _t('AM');
|
||||||
|
@ -62,7 +62,7 @@ function twelveHourTime(date, showSeconds=false) {
|
||||||
return `${hours}:${minutes}${ampm}`;
|
return `${hours}:${minutes}${ampm}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatDate(date, showTwelveHour=false) {
|
export function formatDate(date: Date, showTwelveHour = false): string {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const days = getDaysArray();
|
const days = getDaysArray();
|
||||||
const months = getMonthsArray();
|
const months = getMonthsArray();
|
||||||
|
@ -86,7 +86,7 @@ export function formatDate(date, showTwelveHour=false) {
|
||||||
return formatFullDate(date, showTwelveHour);
|
return formatFullDate(date, showTwelveHour);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatFullDateNoTime(date) {
|
export function formatFullDateNoTime(date: Date): string {
|
||||||
const days = getDaysArray();
|
const days = getDaysArray();
|
||||||
const months = getMonthsArray();
|
const months = getMonthsArray();
|
||||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s', {
|
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 days = getDaysArray();
|
||||||
const months = getMonthsArray();
|
const months = getMonthsArray();
|
||||||
return _t('%(weekDayName)s, %(monthName)s %(day)s %(fullYear)s %(time)s', {
|
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) {
|
if (showTwelveHour) {
|
||||||
return twelveHourTime(date, true);
|
return twelveHourTime(date, true);
|
||||||
}
|
}
|
||||||
return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
|
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) {
|
if (showTwelveHour) {
|
||||||
return twelveHourTime(date);
|
return twelveHourTime(date);
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ export function formatTime(date, showTwelveHour=false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MILLIS_IN_DAY = 86400000;
|
const MILLIS_IN_DAY = 86400000;
|
||||||
export function wantsDateSeparator(prevEventDate, nextEventDate) {
|
export function wantsDateSeparator(prevEventDate: Date, nextEventDate: Date): boolean {
|
||||||
if (!nextEventDate || !prevEventDate) {
|
if (!nextEventDate || !prevEventDate) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
|
@ -29,6 +29,7 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
|
||||||
import url from 'url';
|
import url from 'url';
|
||||||
|
|
||||||
import {MatrixClientPeg} from './MatrixClientPeg';
|
import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
|
import SettingsStore from './settings/SettingsStore';
|
||||||
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks";
|
||||||
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji";
|
||||||
import ReplyThread from "./components/views/elements/ReplyThread";
|
import ReplyThread from "./components/views/elements/ReplyThread";
|
||||||
|
@ -171,7 +172,10 @@ const transformTags: IExtendedSanitizeOptions["transformTags"] = { // custom to
|
||||||
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
// Strip out imgs that aren't `mxc` here instead of using allowedSchemesByTag
|
||||||
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
// because transformTags is used _before_ we filter by allowedSchemesByTag and
|
||||||
// we don't want to allow images with `https?` `src`s.
|
// we don't want to allow images with `https?` `src`s.
|
||||||
if (!attribs.src || !attribs.src.startsWith('mxc://')) {
|
// We also drop inline images (as if they were not present at all) when the "show
|
||||||
|
// images" preference is disabled. Future work might expose some UI to reveal them
|
||||||
|
// like standalone image events have.
|
||||||
|
if (!attribs.src || !attribs.src.startsWith('mxc://') || !SettingsStore.getValue("showImages")) {
|
||||||
return { tagName, attribs: {}};
|
return { tagName, attribs: {}};
|
||||||
}
|
}
|
||||||
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
attribs.src = MatrixClientPeg.get().mxcUrlToHttp(
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016, 2020 Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -14,8 +14,6 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the actual height that an image of dimensions (fullWidth, fullHeight)
|
* Returns the actual height that an image of dimensions (fullWidth, fullHeight)
|
||||||
* will occupy if resized to fit inside a thumbnail bounding box of size
|
* will occupy if resized to fit inside a thumbnail bounding box of size
|
||||||
|
@ -30,11 +28,11 @@ limitations under the License.
|
||||||
* consume in the timeline, when performing scroll offset calcuations
|
* consume in the timeline, when performing scroll offset calcuations
|
||||||
* (e.g. scroll locking)
|
* (e.g. scroll locking)
|
||||||
*/
|
*/
|
||||||
export function thumbHeight(fullWidth, fullHeight, thumbWidth, thumbHeight) {
|
export function thumbHeight(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
|
||||||
if (!fullWidth || !fullHeight) {
|
if (!fullWidth || !fullHeight) {
|
||||||
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
// Cannot calculate thumbnail height for image: missing w/h in metadata. We can't even
|
||||||
// log this because it's spammy
|
// log this because it's spammy
|
||||||
return undefined;
|
return null;
|
||||||
}
|
}
|
||||||
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
if (fullWidth < thumbWidth && fullHeight < thumbHeight) {
|
||||||
// no scaling needs to be applied
|
// no scaling needs to be applied
|
|
@ -23,6 +23,7 @@ import { InvalidStoreError } from "matrix-js-sdk/src/errors";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
import {IMatrixClientCreds, MatrixClientPeg} from './MatrixClientPeg';
|
||||||
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
import EventIndexPeg from './indexing/EventIndexPeg';
|
import EventIndexPeg from './indexing/EventIndexPeg';
|
||||||
import createMatrixClient from './utils/createMatrixClient';
|
import createMatrixClient from './utils/createMatrixClient';
|
||||||
import Analytics from './Analytics';
|
import Analytics from './Analytics';
|
||||||
|
@ -46,6 +47,7 @@ import DeviceListener from "./DeviceListener";
|
||||||
import {Jitsi} from "./widgets/Jitsi";
|
import {Jitsi} from "./widgets/Jitsi";
|
||||||
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
|
||||||
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
|
||||||
|
import CountlyAnalytics from "./CountlyAnalytics";
|
||||||
|
|
||||||
const HOMESERVER_URL_KEY = "mx_hs_url";
|
const HOMESERVER_URL_KEY = "mx_hs_url";
|
||||||
const ID_SERVER_URL_KEY = "mx_is_url";
|
const ID_SERVER_URL_KEY = "mx_is_url";
|
||||||
|
@ -567,6 +569,8 @@ function persistCredentialsToLocalStorage(credentials: IMatrixClientCreds): void
|
||||||
localStorage.setItem("mx_device_id", credentials.deviceId);
|
localStorage.setItem("mx_device_id", credentials.deviceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SecurityCustomisations.persistCredentials?.(credentials);
|
||||||
|
|
||||||
console.log(`Session persisted for ${credentials.userId}`);
|
console.log(`Session persisted for ${credentials.userId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -577,6 +581,10 @@ let _isLoggingOut = false;
|
||||||
*/
|
*/
|
||||||
export function logout(): void {
|
export function logout(): void {
|
||||||
if (!MatrixClientPeg.get()) return;
|
if (!MatrixClientPeg.get()) return;
|
||||||
|
if (!CountlyAnalytics.instance.disabled) {
|
||||||
|
// user has logged out, fall back to anonymous
|
||||||
|
CountlyAnalytics.instance.enable(/* anonymous = */ true);
|
||||||
|
}
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
// logout doesn't work for guest sessions
|
// logout doesn't work for guest sessions
|
||||||
|
|
|
@ -22,6 +22,7 @@ limitations under the License.
|
||||||
import Matrix from "matrix-js-sdk";
|
import Matrix from "matrix-js-sdk";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
import { IMatrixClientCreds } from "./MatrixClientPeg";
|
||||||
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
|
||||||
interface ILoginOptions {
|
interface ILoginOptions {
|
||||||
defaultDeviceDisplayName?: string;
|
defaultDeviceDisplayName?: string;
|
||||||
|
@ -222,11 +223,15 @@ export async function sendLoginRequest(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const creds: IMatrixClientCreds = {
|
||||||
homeserverUrl: hsUrl,
|
homeserverUrl: hsUrl,
|
||||||
identityServerUrl: isUrl,
|
identityServerUrl: isUrl,
|
||||||
userId: data.user_id,
|
userId: data.user_id,
|
||||||
deviceId: data.device_id,
|
deviceId: data.device_id,
|
||||||
accessToken: data.access_token,
|
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 IdentityAuthClient from './IdentityAuthClient';
|
||||||
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
import { crossSigningCallbacks, tryToUnlockSecretStorageWithDehydrationKey } from './SecurityManager';
|
||||||
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
import {SHOW_QR_CODE_METHOD} from "matrix-js-sdk/src/crypto/verification/QRCode";
|
||||||
|
import SecurityCustomisations from "./customisations/Security";
|
||||||
|
|
||||||
export interface IMatrixClientCreds {
|
export interface IMatrixClientCreds {
|
||||||
homeserverUrl: string;
|
homeserverUrl: string;
|
||||||
|
@ -100,6 +101,12 @@ export interface IMatrixClientPeg {
|
||||||
*/
|
*/
|
||||||
currentUserIsJustRegistered(): boolean;
|
currentUserIsJustRegistered(): boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the current user has been registered by this device then this
|
||||||
|
* returns a boolean of whether it was within the last N hours given.
|
||||||
|
*/
|
||||||
|
userRegisteredWithinLastHours(hours: number): boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Replace this MatrixClientPeg's client with a client instance that has
|
* Replace this MatrixClientPeg's client with a client instance that has
|
||||||
* homeserver / identity server URLs and active credentials
|
* homeserver / identity server URLs and active credentials
|
||||||
|
@ -150,6 +157,9 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
|
|
||||||
public setJustRegisteredUserId(uid: string): void {
|
public setJustRegisteredUserId(uid: string): void {
|
||||||
this.justRegisteredUserId = uid;
|
this.justRegisteredUserId = uid;
|
||||||
|
if (uid) {
|
||||||
|
window.localStorage.setItem("mx_registration_time", String(new Date().getTime()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public currentUserIsJustRegistered(): boolean {
|
public currentUserIsJustRegistered(): boolean {
|
||||||
|
@ -159,6 +169,15 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public userRegisteredWithinLastHours(hours: number): boolean {
|
||||||
|
try {
|
||||||
|
const date = new Date(window.localStorage.getItem("mx_registration_time"));
|
||||||
|
return ((new Date().getTime() - date.getTime()) / 36e5) <= hours;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public replaceUsingCreds(creds: IMatrixClientCreds): void {
|
public replaceUsingCreds(creds: IMatrixClientCreds): void {
|
||||||
this.currentClientCreds = creds;
|
this.currentClientCreds = creds;
|
||||||
this.createClient(creds);
|
this.createClient(creds);
|
||||||
|
@ -273,7 +292,10 @@ class _MatrixClientPeg implements IMatrixClientPeg {
|
||||||
// These are always installed regardless of the labs flag so that
|
// These are always installed regardless of the labs flag so that
|
||||||
// cross-signing features can toggle on without reloading and also be
|
// cross-signing features can toggle on without reloading and also be
|
||||||
// accessed immediately after login.
|
// accessed immediately after login.
|
||||||
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks);
|
const customisedCallbacks = {
|
||||||
|
getDehydrationKey: SecurityCustomisations.getDehydrationKey,
|
||||||
|
};
|
||||||
|
Object.assign(opts.cryptoCallbacks, crossSigningCallbacks, customisedCallbacks);
|
||||||
|
|
||||||
this.matrixClient = createMatrixClient(opts);
|
this.matrixClient = createMatrixClient(opts);
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ import AsyncWrapper from './AsyncWrapper';
|
||||||
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
|
||||||
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
|
||||||
|
|
||||||
interface IModal<T extends any[]> {
|
export interface IModal<T extends any[]> {
|
||||||
elem: React.ReactNode;
|
elem: React.ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
beforeClosePromise?: Promise<boolean>;
|
beforeClosePromise?: Promise<boolean>;
|
||||||
|
@ -38,7 +38,7 @@ interface IModal<T extends any[]> {
|
||||||
close(...args: T): void;
|
close(...args: T): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IHandle<T extends any[]> {
|
export interface IHandle<T extends any[]> {
|
||||||
finished: Promise<T>;
|
finished: Promise<T>;
|
||||||
close(...args: T): void;
|
close(...args: T): void;
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,7 +218,7 @@ export const Notifier = {
|
||||||
// calculated value. It is determined based upon whether or not the master rule is enabled
|
// 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.
|
// 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
|
// make sure that we persist the current setting audio_enabled setting
|
||||||
// before changing anything
|
// before changing anything
|
||||||
|
@ -287,7 +287,7 @@ export const Notifier = {
|
||||||
setPromptHidden: function(hidden: boolean, persistent = true) {
|
setPromptHidden: function(hidden: boolean, persistent = true) {
|
||||||
this.toolbarHidden = hidden;
|
this.toolbarHidden = hidden;
|
||||||
|
|
||||||
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', hidden);
|
Analytics.trackEvent('Notifier', 'Set Toolbar Hidden', String(hidden));
|
||||||
|
|
||||||
hideNotificationsToast();
|
hideNotificationsToast();
|
||||||
|
|
||||||
|
|
|
@ -19,30 +19,34 @@ limitations under the License.
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import dis from "./dispatcher/dispatcher";
|
import dis from "./dispatcher/dispatcher";
|
||||||
import Timer from './utils/Timer';
|
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 UNAVAILABLE_TIME_MS = 3 * 60 * 1000; // 3 mins
|
||||||
const PRESENCE_STATES = ["online", "offline", "unavailable"];
|
|
||||||
|
enum State {
|
||||||
|
Online = "online",
|
||||||
|
Offline = "offline",
|
||||||
|
Unavailable = "unavailable",
|
||||||
|
}
|
||||||
|
|
||||||
class Presence {
|
class Presence {
|
||||||
constructor() {
|
private unavailableTimer: Timer = null;
|
||||||
this._activitySignal = null;
|
private dispatcherRef: string = null;
|
||||||
this._unavailableTimer = null;
|
private state: State = null;
|
||||||
this._onAction = this._onAction.bind(this);
|
|
||||||
this._dispatcherRef = null;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Start listening the user activity to evaluate his presence state.
|
* Start listening the user activity to evaluate his presence state.
|
||||||
* Any state change will be sent to the homeserver.
|
* Any state change will be sent to the homeserver.
|
||||||
*/
|
*/
|
||||||
async start() {
|
public async start() {
|
||||||
this._unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
|
this.unavailableTimer = new Timer(UNAVAILABLE_TIME_MS);
|
||||||
// the user_activity_start action starts the timer
|
// the user_activity_start action starts the timer
|
||||||
this._dispatcherRef = dis.register(this._onAction);
|
this.dispatcherRef = dis.register(this.onAction);
|
||||||
while (this._unavailableTimer) {
|
while (this.unavailableTimer) {
|
||||||
try {
|
try {
|
||||||
await this._unavailableTimer.finished();
|
await this.unavailableTimer.finished();
|
||||||
this.setState("unavailable");
|
this.setState(State.Unavailable);
|
||||||
} catch (e) { /* aborted, stop got called */ }
|
} catch (e) { /* aborted, stop got called */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,14 +54,14 @@ class Presence {
|
||||||
/**
|
/**
|
||||||
* Stop tracking user activity
|
* Stop tracking user activity
|
||||||
*/
|
*/
|
||||||
stop() {
|
public stop() {
|
||||||
if (this._dispatcherRef) {
|
if (this.dispatcherRef) {
|
||||||
dis.unregister(this._dispatcherRef);
|
dis.unregister(this.dispatcherRef);
|
||||||
this._dispatcherRef = null;
|
this.dispatcherRef = null;
|
||||||
}
|
}
|
||||||
if (this._unavailableTimer) {
|
if (this.unavailableTimer) {
|
||||||
this._unavailableTimer.abort();
|
this.unavailableTimer.abort();
|
||||||
this._unavailableTimer = null;
|
this.unavailableTimer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,14 +69,14 @@ class Presence {
|
||||||
* Get the current presence state.
|
* Get the current presence state.
|
||||||
* @returns {string} the presence state (see PRESENCE enum)
|
* @returns {string} the presence state (see PRESENCE enum)
|
||||||
*/
|
*/
|
||||||
getState() {
|
public getState() {
|
||||||
return this.state;
|
return this.state;
|
||||||
}
|
}
|
||||||
|
|
||||||
_onAction(payload) {
|
private onAction = (payload: ActionPayload) => {
|
||||||
if (payload.action === 'user_activity') {
|
if (payload.action === 'user_activity') {
|
||||||
this.setState("online");
|
this.setState(State.Online);
|
||||||
this._unavailableTimer.restart();
|
this.unavailableTimer.restart();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,13 +85,11 @@ class Presence {
|
||||||
* If the state has changed, the homeserver will be notified.
|
* If the state has changed, the homeserver will be notified.
|
||||||
* @param {string} newState the new presence state (see PRESENCE enum)
|
* @param {string} newState the new presence state (see PRESENCE enum)
|
||||||
*/
|
*/
|
||||||
async setState(newState) {
|
private async setState(newState: State) {
|
||||||
if (newState === this.state) {
|
if (newState === this.state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (PRESENCE_STATES.indexOf(newState) === -1) {
|
|
||||||
throw new Error("Bad presence state: " + newState);
|
|
||||||
}
|
|
||||||
const oldState = this.state;
|
const oldState = this.state;
|
||||||
this.state = newState;
|
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
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
export function levelRoleMap(usersDefault) {
|
export function levelRoleMap(usersDefault: number) {
|
||||||
return {
|
return {
|
||||||
undefined: _t('Default'),
|
undefined: _t('Default'),
|
||||||
0: _t('Restricted'),
|
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);
|
const LEVEL_ROLE_MAP = levelRoleMap(usersDefault);
|
||||||
if (LEVEL_ROLE_MAP[level]) {
|
if (LEVEL_ROLE_MAP[level]) {
|
||||||
return 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}));
|
return inviter.invite(addrs).then(states => Promise.resolve({states, inviter}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showStartChatInviteDialog() {
|
export function showStartChatInviteDialog(initialText) {
|
||||||
// This dialog handles the room creation internally - we don't need to worry about it.
|
// This dialog handles the room creation internally - we don't need to worry about it.
|
||||||
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
const InviteDialog = sdk.getComponent("dialogs.InviteDialog");
|
||||||
Modal.createTrackedDialog(
|
Modal.createTrackedDialog(
|
||||||
'Start DM', '', InviteDialog, {kind: KIND_DM},
|
'Start DM', '', InviteDialog, {kind: KIND_DM, initialText},
|
||||||
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,9 @@ import {MatrixClientPeg} from './MatrixClientPeg';
|
||||||
* if any. This could be the canonical alias if one exists, otherwise
|
* if any. This could be the canonical alias if one exists, otherwise
|
||||||
* an alias selected arbitrarily but deterministically from the list
|
* an alias selected arbitrarily but deterministically from the list
|
||||||
* of aliases. Otherwise return null;
|
* of aliases. Otherwise return null;
|
||||||
|
*
|
||||||
|
* @param {Object} room The room object
|
||||||
|
* @returns {string} A display alias for the given room
|
||||||
*/
|
*/
|
||||||
export function getDisplayAliasForRoom(room) {
|
export function getDisplayAliasForRoom(room) {
|
||||||
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
return room.getCanonicalAlias() || room.getAltAliases()[0];
|
||||||
|
|
|
@ -360,7 +360,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
let oldestEventFrom = previousSearchResult.oldestEventFrom;
|
||||||
response.highlights = previousSearchResult.highlights;
|
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
|
// 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
|
// the local index. Note where our oldest event came from, we shall
|
||||||
// fetch the next batch of events from the other source.
|
// fetch the next batch of events from the other source.
|
||||||
|
@ -379,7 +379,7 @@ function combineEvents(previousSearchResult, localEvents = undefined, serverEven
|
||||||
oldestEventFrom = "local";
|
oldestEventFrom = "local";
|
||||||
}
|
}
|
||||||
combineEventSources(previousSearchResult, response, localEvents.results, cachedEvents);
|
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,
|
// This is a pagination call fetching more events from the server,
|
||||||
// meaning that our oldest event was in the local index.
|
// meaning that our oldest event was in the local index.
|
||||||
// Change the source of the oldest event if our server event is older
|
// 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;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreEncryptionInfo(searchResultSlice) {
|
function restoreEncryptionInfo(searchResultSlice = []) {
|
||||||
for (let i = 0; i < searchResultSlice.length; i++) {
|
for (let i = 0; i < searchResultSlice.length; i++) {
|
||||||
const timeline = searchResultSlice[i].context.getTimeline();
|
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.
|
// Let the client process the combined result.
|
||||||
const result = client._processRoomEventsSearch(searchResult, response);
|
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 { deriveKey } from 'matrix-js-sdk/src/crypto/key_passphrase';
|
||||||
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
import { decodeRecoveryKey } from 'matrix-js-sdk/src/crypto/recoverykey';
|
||||||
import { _t } from './languageHandler';
|
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 { isSecureBackupRequired } from './utils/WellKnownUtils';
|
||||||
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
|
import AccessSecretStorageDialog from './components/views/dialogs/security/AccessSecretStorageDialog';
|
||||||
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog';
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
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
|
// 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
|
// 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) {
|
if (nonInteractive) {
|
||||||
throw new Error("Could not unlock non-interactively");
|
throw new Error("Could not unlock non-interactively");
|
||||||
}
|
}
|
||||||
|
@ -158,6 +166,12 @@ export async function getDehydrationKey(
|
||||||
keyInfo: ISecretStorageKeyInfo,
|
keyInfo: ISecretStorageKeyInfo,
|
||||||
checkFunc: (Uint8Array) => void,
|
checkFunc: (Uint8Array) => void,
|
||||||
): Promise<Uint8Array> {
|
): Promise<Uint8Array> {
|
||||||
|
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
|
||||||
|
if (keyFromCustomisations) {
|
||||||
|
console.log("Using key from security customisations (dehydration)")
|
||||||
|
return keyFromCustomisations;
|
||||||
|
}
|
||||||
|
|
||||||
const inputToKey = makeInputToKey(keyInfo);
|
const inputToKey = makeInputToKey(keyInfo);
|
||||||
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
const { finished } = Modal.createTrackedDialog("Access Secret Storage dialog", "",
|
||||||
AccessSecretStorageDialog,
|
AccessSecretStorageDialog,
|
||||||
|
@ -352,14 +366,19 @@ export async function accessSecretStorage(func = async () => { }, forceReset = f
|
||||||
}
|
}
|
||||||
console.log("Setting dehydration key");
|
console.log("Setting dehydration key");
|
||||||
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
|
await cli.setDehydrationKey(secretStorageKeys[keyId], dehydrationKeyInfo, "Backup device");
|
||||||
|
} else if (!keyId) {
|
||||||
|
console.warn("Not setting dehydration key: no SSSS key found");
|
||||||
} else {
|
} 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
|
// `return await` needed here to ensure `finally` block runs after the
|
||||||
// inner operation completes.
|
// inner operation completes.
|
||||||
return await func();
|
return await func();
|
||||||
|
} catch (e) {
|
||||||
|
SecurityCustomisations.catchAccessSecretStorageError?.(e);
|
||||||
|
console.error(e);
|
||||||
} finally {
|
} finally {
|
||||||
// Clear secret storage key cache now that work is complete
|
// Clear secret storage key cache now that work is complete
|
||||||
secretStorageBeingAccessed = false;
|
secretStorageBeingAccessed = false;
|
||||||
|
|
|
@ -50,8 +50,8 @@ class Skinner {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// components have to be functions.
|
// components have to be functions or forwardRef objects with a render function.
|
||||||
const validType = typeof comp === 'function';
|
const validType = typeof comp === 'function' || comp.render;
|
||||||
if (!validType) {
|
if (!validType) {
|
||||||
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
throw new Error(`Not a valid component: ${name} (type = ${typeof(comp)}).`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
|
||||||
import SdkConfig from "./SdkConfig";
|
import SdkConfig from "./SdkConfig";
|
||||||
import SettingsStore from "./settings/SettingsStore";
|
import SettingsStore from "./settings/SettingsStore";
|
||||||
import {UIFeature} from "./settings/UIFeature";
|
import {UIFeature} from "./settings/UIFeature";
|
||||||
|
import CallHandler from "./CallHandler";
|
||||||
|
|
||||||
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
// XXX: workaround for https://github.com/microsoft/TypeScript/issues/31816
|
||||||
interface HTMLInputEvent extends Event {
|
interface HTMLInputEvent extends Event {
|
||||||
|
@ -517,6 +518,7 @@ export const Commands = [
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_alias: roomAlias,
|
room_alias: roomAlias,
|
||||||
auto_join: true,
|
auto_join: true,
|
||||||
|
_type: "slash_command", // instrumentation
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
} else if (params[0][0] === '!') {
|
} else if (params[0][0] === '!') {
|
||||||
|
@ -531,6 +533,7 @@ export const Commands = [
|
||||||
},
|
},
|
||||||
via_servers: viaServers, // for the rejoin button
|
via_servers: viaServers, // for the rejoin button
|
||||||
auto_join: true,
|
auto_join: true,
|
||||||
|
_type: "slash_command", // instrumentation
|
||||||
});
|
});
|
||||||
return success();
|
return success();
|
||||||
} else if (isPermalink) {
|
} else if (isPermalink) {
|
||||||
|
@ -555,6 +558,7 @@ export const Commands = [
|
||||||
const dispatch = {
|
const dispatch = {
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
auto_join: true,
|
auto_join: true,
|
||||||
|
_type: "slash_command", // instrumentation
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entity[0] === '!') dispatch["room_id"] = entity;
|
if (entity[0] === '!') dispatch["room_id"] = entity;
|
||||||
|
@ -998,14 +1002,29 @@ export const Commands = [
|
||||||
description: _td("Opens chat with the given user"),
|
description: _td("Opens chat with the given user"),
|
||||||
args: "<user-id>",
|
args: "<user-id>",
|
||||||
runFn: function(roomId, userId) {
|
runFn: function(roomId, userId) {
|
||||||
if (!userId || !userId.startsWith("@") || !userId.includes(":")) {
|
// easter-egg for now: look up phone numbers through the thirdparty API
|
||||||
|
// (very dumb phone number detection...)
|
||||||
|
const isPhoneNumber = userId && /^\+?[0123456789]+$/.test(userId);
|
||||||
|
if (!userId || (!userId.startsWith("@") || !userId.includes(":")) && !isPhoneNumber) {
|
||||||
return reject(this.getUsage());
|
return reject(this.getUsage());
|
||||||
}
|
}
|
||||||
|
|
||||||
return success((async () => {
|
return success((async () => {
|
||||||
|
if (isPhoneNumber) {
|
||||||
|
const results = await MatrixClientPeg.get().getThirdpartyUser('im.vector.protocol.pstn', {
|
||||||
|
'm.id.phone': userId,
|
||||||
|
});
|
||||||
|
if (!results || results.length === 0 || !results[0].userid) {
|
||||||
|
throw new Error("Unable to find Matrix ID for phone number");
|
||||||
|
}
|
||||||
|
userId = results[0].userid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roomId = await ensureDMExists(MatrixClientPeg.get(), userId);
|
||||||
|
|
||||||
dis.dispatch({
|
dis.dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
room_id: await ensureDMExists(MatrixClientPeg.get(), userId),
|
room_id: roomId,
|
||||||
});
|
});
|
||||||
})());
|
})());
|
||||||
},
|
},
|
||||||
|
@ -1039,6 +1058,32 @@ export const Commands = [
|
||||||
},
|
},
|
||||||
category: CommandCategories.actions,
|
category: CommandCategories.actions,
|
||||||
}),
|
}),
|
||||||
|
new Command({
|
||||||
|
command: "holdcall",
|
||||||
|
description: _td("Places the call in the current room on hold"),
|
||||||
|
category: CommandCategories.other,
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||||
|
if (!call) {
|
||||||
|
return reject("No active call in this room");
|
||||||
|
}
|
||||||
|
call.setRemoteOnHold(true);
|
||||||
|
return success();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
new Command({
|
||||||
|
command: "unholdcall",
|
||||||
|
description: _td("Takes the call in the current room off hold"),
|
||||||
|
category: CommandCategories.other,
|
||||||
|
runFn: function(roomId, args) {
|
||||||
|
const call = CallHandler.sharedInstance().getCallForRoom(roomId);
|
||||||
|
if (!call) {
|
||||||
|
return reject("No active call in this room");
|
||||||
|
}
|
||||||
|
call.setRemoteOnHold(false);
|
||||||
|
return success();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
// Command definitions for autocompletion ONLY:
|
// Command definitions for autocompletion ONLY:
|
||||||
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
// /me is special because its not handled by SlashCommands.js and is instead done inside the Composer classes
|
||||||
|
|
|
@ -198,59 +198,30 @@ function textForRelatedGroupsEvent(ev) {
|
||||||
function textForServerACLEvent(ev) {
|
function textForServerACLEvent(ev) {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
const changes = [];
|
|
||||||
const current = ev.getContent();
|
const current = ev.getContent();
|
||||||
const prev = {
|
const prev = {
|
||||||
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
|
deny: Array.isArray(prevContent.deny) ? prevContent.deny : [],
|
||||||
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
|
allow: Array.isArray(prevContent.allow) ? prevContent.allow : [],
|
||||||
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
allow_ip_literals: !(prevContent.allow_ip_literals === false),
|
||||||
};
|
};
|
||||||
|
|
||||||
let text = "";
|
let text = "";
|
||||||
if (prev.deny.length === 0 && prev.allow.length === 0) {
|
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 {
|
} 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)) {
|
if (!Array.isArray(current.allow)) {
|
||||||
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) {
|
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)) {
|
return text;
|
||||||
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(" ");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function textForMessageEvent(ev) {
|
function textForMessageEvent(ev) {
|
||||||
|
@ -329,14 +300,27 @@ function textForCallHangupEvent(event) {
|
||||||
reason = _t('(not supported by this browser)');
|
reason = _t('(not supported by this browser)');
|
||||||
} else if (eventContent.reason) {
|
} else if (eventContent.reason) {
|
||||||
if (eventContent.reason === "ice_failed") {
|
if (eventContent.reason === "ice_failed") {
|
||||||
|
// We couldn't establish a connection at all
|
||||||
reason = _t('(could not connect media)');
|
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") {
|
} else if (eventContent.reason === "invite_timeout") {
|
||||||
reason = _t('(no answer)');
|
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
|
// workaround for https://github.com/vector-im/element-web/issues/5178
|
||||||
// it seems Android randomly sets a reason of "user hangup" which is
|
// it seems Android randomly sets a reason of "user hangup" which is
|
||||||
// interpreted as an error code :(
|
// interpreted as an error code :(
|
||||||
// https://github.com/vector-im/riot-android/issues/2623
|
// https://github.com/vector-im/riot-android/issues/2623
|
||||||
|
// Also the correct hangup code as of VoIP v1 (with underscore)
|
||||||
reason = '';
|
reason = '';
|
||||||
} else {
|
} else {
|
||||||
reason = _t('(unknown failure: %(reason)s)', {reason: eventContent.reason});
|
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;
|
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) {
|
function textForCallInviteEvent(event) {
|
||||||
const senderName = event.sender ? event.sender.name : _t('Someone');
|
const senderName = event.sender ? event.sender.name : _t('Someone');
|
||||||
// FIXME: Find a better way to determine this from the event?
|
// FIXME: Find a better way to determine this from the event?
|
||||||
|
@ -574,6 +563,7 @@ const handlers = {
|
||||||
'm.call.invite': textForCallInviteEvent,
|
'm.call.invite': textForCallInviteEvent,
|
||||||
'm.call.answer': textForCallAnswerEvent,
|
'm.call.answer': textForCallAnswerEvent,
|
||||||
'm.call.hangup': textForCallHangupEvent,
|
'm.call.hangup': textForCallHangupEvent,
|
||||||
|
'm.call.reject': textForCallRejectEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateHandlers = {
|
const stateHandlers = {
|
||||||
|
|
|
@ -16,12 +16,14 @@ limitations under the License.
|
||||||
|
|
||||||
import {MatrixClientPeg} from "./MatrixClientPeg";
|
import {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import shouldHideEvent from './shouldHideEvent';
|
import shouldHideEvent from './shouldHideEvent';
|
||||||
import * as sdk from "./index";
|
|
||||||
import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
import {haveTileForEvent} from "./components/views/rooms/EventTile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true iff this event arriving in a room should affect the room's
|
* Returns true iff this event arriving in a room should affect the room's
|
||||||
* count of unread messages
|
* count of unread messages
|
||||||
|
*
|
||||||
|
* @param {Object} ev The event
|
||||||
|
* @returns {boolean} True if the given event should affect the unread message count
|
||||||
*/
|
*/
|
||||||
export function eventTriggersUnreadCount(ev) {
|
export function eventTriggersUnreadCount(ev) {
|
||||||
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
|
||||||
|
|
|
@ -38,26 +38,23 @@ const RECENTLY_ACTIVE_THRESHOLD_MS = 2 * 60 * 1000;
|
||||||
* see doc on the userActive* functions for what these mean.
|
* see doc on the userActive* functions for what these mean.
|
||||||
*/
|
*/
|
||||||
export default class UserActivity {
|
export default class UserActivity {
|
||||||
constructor(windowObj, documentObj) {
|
private readonly activeNowTimeout: Timer;
|
||||||
this._window = windowObj;
|
private readonly activeRecentlyTimeout: Timer;
|
||||||
this._document = documentObj;
|
private attachedActiveNowTimers: Timer[] = [];
|
||||||
|
private attachedActiveRecentlyTimers: Timer[] = [];
|
||||||
|
private lastScreenX = 0;
|
||||||
|
private lastScreenY = 0;
|
||||||
|
|
||||||
this._attachedActiveNowTimers = [];
|
constructor(private readonly window: Window, private readonly document: Document) {
|
||||||
this._attachedActiveRecentlyTimers = [];
|
this.activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS);
|
||||||
this._activeNowTimeout = new Timer(CURRENTLY_ACTIVE_THRESHOLD_MS);
|
this.activeRecentlyTimeout = new Timer(RECENTLY_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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static sharedInstance() {
|
static sharedInstance() {
|
||||||
if (global.mxUserActivity === undefined) {
|
if (window.mxUserActivity === undefined) {
|
||||||
global.mxUserActivity = new UserActivity(window, document);
|
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.
|
* later on when the user does become active.
|
||||||
* @param {Timer} timer the timer to use
|
* @param {Timer} timer the timer to use
|
||||||
*/
|
*/
|
||||||
timeWhileActiveNow(timer) {
|
public timeWhileActiveNow(timer: Timer) {
|
||||||
this._timeWhile(timer, this._attachedActiveNowTimers);
|
this.timeWhile(timer, this.attachedActiveNowTimers);
|
||||||
if (this.userActiveNow()) {
|
if (this.userActiveNow()) {
|
||||||
timer.start();
|
timer.start();
|
||||||
}
|
}
|
||||||
|
@ -85,14 +82,14 @@ export default class UserActivity {
|
||||||
* later on when the user does become active.
|
* later on when the user does become active.
|
||||||
* @param {Timer} timer the timer to use
|
* @param {Timer} timer the timer to use
|
||||||
*/
|
*/
|
||||||
timeWhileActiveRecently(timer) {
|
public timeWhileActiveRecently(timer: Timer) {
|
||||||
this._timeWhile(timer, this._attachedActiveRecentlyTimers);
|
this.timeWhile(timer, this.attachedActiveRecentlyTimers);
|
||||||
if (this.userActiveRecently()) {
|
if (this.userActiveRecently()) {
|
||||||
timer.start();
|
timer.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_timeWhile(timer, attachedTimers) {
|
private timeWhile(timer: Timer, attachedTimers: Timer[]) {
|
||||||
// important this happens first
|
// important this happens first
|
||||||
const index = attachedTimers.indexOf(timer);
|
const index = attachedTimers.indexOf(timer);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
@ -112,36 +109,36 @@ export default class UserActivity {
|
||||||
/**
|
/**
|
||||||
* Start listening to user activity
|
* Start listening to user activity
|
||||||
*/
|
*/
|
||||||
start() {
|
public start() {
|
||||||
this._document.addEventListener('mousedown', this._onUserActivity);
|
this.document.addEventListener('mousedown', this.onUserActivity);
|
||||||
this._document.addEventListener('mousemove', this._onUserActivity);
|
this.document.addEventListener('mousemove', this.onUserActivity);
|
||||||
this._document.addEventListener('keydown', this._onUserActivity);
|
this.document.addEventListener('keydown', this.onUserActivity);
|
||||||
this._document.addEventListener("visibilitychange", this._onPageVisibilityChanged);
|
this.document.addEventListener("visibilitychange", this.onPageVisibilityChanged);
|
||||||
this._window.addEventListener("blur", this._onWindowBlurred);
|
this.window.addEventListener("blur", this.onWindowBlurred);
|
||||||
this._window.addEventListener("focus", this._onUserActivity);
|
this.window.addEventListener("focus", this.onUserActivity);
|
||||||
// can't use document.scroll here because that's only the document
|
// can't use document.scroll here because that's only the document
|
||||||
// itself being scrolled. Need to use addEventListener's useCapture.
|
// itself being scrolled. Need to use addEventListener's useCapture.
|
||||||
// also this needs to be the wheel event, not scroll, as scroll is
|
// also this needs to be the wheel event, not scroll, as scroll is
|
||||||
// fired when the view scrolls down for a new message.
|
// fired when the view scrolls down for a new message.
|
||||||
this._window.addEventListener('wheel', this._onUserActivity, {
|
this.window.addEventListener('wheel', this.onUserActivity, {
|
||||||
passive: true, capture: true,
|
passive: true,
|
||||||
|
capture: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop tracking user activity
|
* Stop tracking user activity
|
||||||
*/
|
*/
|
||||||
stop() {
|
public stop() {
|
||||||
this._document.removeEventListener('mousedown', this._onUserActivity);
|
this.document.removeEventListener('mousedown', this.onUserActivity);
|
||||||
this._document.removeEventListener('mousemove', this._onUserActivity);
|
this.document.removeEventListener('mousemove', this.onUserActivity);
|
||||||
this._document.removeEventListener('keydown', this._onUserActivity);
|
this.document.removeEventListener('keydown', this.onUserActivity);
|
||||||
this._window.removeEventListener('wheel', this._onUserActivity, {
|
this.window.removeEventListener('wheel', this.onUserActivity, {
|
||||||
passive: true, capture: true,
|
capture: true,
|
||||||
});
|
});
|
||||||
|
this.document.removeEventListener("visibilitychange", this.onPageVisibilityChanged);
|
||||||
this._document.removeEventListener("visibilitychange", this._onPageVisibilityChanged);
|
this.window.removeEventListener("blur", this.onWindowBlurred);
|
||||||
this._window.removeEventListener("blur", this._onWindowBlurred);
|
this.window.removeEventListener("focus", this.onUserActivity);
|
||||||
this._window.removeEventListener("focus", this._onUserActivity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -151,8 +148,8 @@ export default class UserActivity {
|
||||||
* user's attention at any given moment.
|
* user's attention at any given moment.
|
||||||
* @returns {boolean} true if user is currently 'active'
|
* @returns {boolean} true if user is currently 'active'
|
||||||
*/
|
*/
|
||||||
userActiveNow() {
|
public userActiveNow() {
|
||||||
return this._activeNowTimeout.isRunning();
|
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).
|
* (or they may have gone to make tea and left the window focused).
|
||||||
* @returns {boolean} true if user has been active recently
|
* @returns {boolean} true if user has been active recently
|
||||||
*/
|
*/
|
||||||
userActiveRecently() {
|
public userActiveRecently() {
|
||||||
return this._activeRecentlyTimeout.isRunning();
|
return this.activeRecentlyTimeout.isRunning();
|
||||||
}
|
}
|
||||||
|
|
||||||
_onPageVisibilityChanged(e) {
|
private onPageVisibilityChanged = e => {
|
||||||
if (this._document.visibilityState === "hidden") {
|
if (this.document.visibilityState === "hidden") {
|
||||||
this._activeNowTimeout.abort();
|
this.activeNowTimeout.abort();
|
||||||
this._activeRecentlyTimeout.abort();
|
this.activeRecentlyTimeout.abort();
|
||||||
} else {
|
} else {
|
||||||
this._onUserActivity(e);
|
this.onUserActivity(e);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
_onWindowBlurred() {
|
private onWindowBlurred = () => {
|
||||||
this._activeNowTimeout.abort();
|
this.activeNowTimeout.abort();
|
||||||
this._activeRecentlyTimeout.abort();
|
this.activeRecentlyTimeout.abort();
|
||||||
}
|
};
|
||||||
|
|
||||||
_onUserActivity(event) {
|
private onUserActivity = (event: MouseEvent) => {
|
||||||
// ignore anything if the window isn't focused
|
// 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 && event.type === "mousemove") {
|
||||||
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
|
if (event.screenX === this.lastScreenX && event.screenY === this.lastScreenY) {
|
||||||
|
@ -195,25 +192,25 @@ export default class UserActivity {
|
||||||
}
|
}
|
||||||
|
|
||||||
dis.dispatch({action: 'user_activity'});
|
dis.dispatch({action: 'user_activity'});
|
||||||
if (!this._activeNowTimeout.isRunning()) {
|
if (!this.activeNowTimeout.isRunning()) {
|
||||||
this._activeNowTimeout.start();
|
this.activeNowTimeout.start();
|
||||||
dis.dispatch({action: 'user_activity_start'});
|
dis.dispatch({action: 'user_activity_start'});
|
||||||
|
|
||||||
this._runTimersUntilTimeout(this._attachedActiveNowTimers, this._activeNowTimeout);
|
UserActivity.runTimersUntilTimeout(this.attachedActiveNowTimers, this.activeNowTimeout);
|
||||||
} else {
|
} else {
|
||||||
this._activeNowTimeout.restart();
|
this.activeNowTimeout.restart();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._activeRecentlyTimeout.isRunning()) {
|
if (!this.activeRecentlyTimeout.isRunning()) {
|
||||||
this._activeRecentlyTimeout.start();
|
this.activeRecentlyTimeout.start();
|
||||||
|
|
||||||
this._runTimersUntilTimeout(this._attachedActiveRecentlyTimers, this._activeRecentlyTimeout);
|
UserActivity.runTimersUntilTimeout(this.attachedActiveRecentlyTimers, this.activeRecentlyTimeout);
|
||||||
} else {
|
} else {
|
||||||
this._activeRecentlyTimeout.restart();
|
this.activeRecentlyTimeout.restart();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
async _runTimersUntilTimeout(attachedTimers, timeout) {
|
private static async runTimersUntilTimeout(attachedTimers: Timer[], timeout: Timer) {
|
||||||
attachedTimers.forEach((t) => t.start());
|
attachedTimers.forEach((t) => t.start());
|
||||||
try {
|
try {
|
||||||
await timeout.finished();
|
await timeout.finished();
|
|
@ -14,19 +14,18 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
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 {MatrixClientPeg} from "./MatrixClientPeg";
|
||||||
import { _t } from './languageHandler';
|
import { _t } from './languageHandler';
|
||||||
|
|
||||||
export function usersTypingApartFromMeAndIgnored(room) {
|
export function usersTypingApartFromMeAndIgnored(room: Room): RoomMember[] {
|
||||||
return usersTyping(
|
return usersTyping(room, [MatrixClientPeg.get().getUserId()].concat(MatrixClientPeg.get().getIgnoredUsers()));
|
||||||
room, [MatrixClientPeg.get().credentials.userId].concat(MatrixClientPeg.get().getIgnoredUsers()),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function usersTypingApartFromMe(room) {
|
export function usersTypingApartFromMe(room: Room): RoomMember[] {
|
||||||
return usersTyping(
|
return usersTyping(room, [MatrixClientPeg.get().getUserId()]);
|
||||||
room, [MatrixClientPeg.get().credentials.userId],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,15 +33,11 @@ export function usersTypingApartFromMe(room) {
|
||||||
* to exclude, return a list of user objects who are typing.
|
* to exclude, return a list of user objects who are typing.
|
||||||
* @param {Room} room: room object to get users from.
|
* @param {Room} room: room object to get users from.
|
||||||
* @param {string[]} exclude: list of user mxids to exclude.
|
* @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 = [];
|
const whoIsTyping = [];
|
||||||
|
|
||||||
if (exclude === undefined) {
|
|
||||||
exclude = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const memberKeys = Object.keys(room.currentState.members);
|
const memberKeys = Object.keys(room.currentState.members);
|
||||||
for (let i = 0; i < memberKeys.length; ++i) {
|
for (let i = 0; i < memberKeys.length; ++i) {
|
||||||
const userId = memberKeys[i];
|
const userId = memberKeys[i];
|
||||||
|
@ -57,20 +52,21 @@ export function usersTyping(room, exclude) {
|
||||||
return whoIsTyping;
|
return whoIsTyping;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function whoIsTypingString(whoIsTyping, limit) {
|
export function whoIsTypingString(whoIsTyping: RoomMember[], limit: number): string {
|
||||||
let othersCount = 0;
|
let othersCount = 0;
|
||||||
if (whoIsTyping.length > limit) {
|
if (whoIsTyping.length > limit) {
|
||||||
othersCount = whoIsTyping.length - limit + 1;
|
othersCount = whoIsTyping.length - limit + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (whoIsTyping.length === 0) {
|
if (whoIsTyping.length === 0) {
|
||||||
return '';
|
return '';
|
||||||
} else if (whoIsTyping.length === 1) {
|
} else if (whoIsTyping.length === 1) {
|
||||||
return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name});
|
return _t('%(displayName)s is typing …', {displayName: whoIsTyping[0].name});
|
||||||
}
|
}
|
||||||
const names = whoIsTyping.map(function(m) {
|
|
||||||
return m.name;
|
const names = whoIsTyping.map(m => m.name);
|
||||||
});
|
|
||||||
if (othersCount>=1) {
|
if (othersCount >= 1) {
|
||||||
return _t('%(names)s and %(count)s others are typing …', {
|
return _t('%(names)s and %(count)s others are typing …', {
|
||||||
names: names.slice(0, limit - 1).join(', '),
|
names: names.slice(0, limit - 1).join(', '),
|
||||||
count: othersCount,
|
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
|
// onFocus should be called when the index gained focus in any manner
|
||||||
// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}`
|
// isActive should be used to set tabIndex in a manner such as `tabIndex={isActive ? 0 : -1}`
|
||||||
// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition
|
// ref should be passed to a DOM node which will be used for DOM compareDocumentPosition
|
||||||
export const useRovingTabIndex = (inputRef: Ref): [FocusHandler, boolean, Ref] => {
|
export const useRovingTabIndex = (inputRef?: Ref): [FocusHandler, boolean, Ref] => {
|
||||||
const context = useContext(RovingTabIndexContext);
|
const context = useContext(RovingTabIndexContext);
|
||||||
let ref = useRef<HTMLElement>(null);
|
let ref = useRef<HTMLElement>(null);
|
||||||
|
|
||||||
|
|
|
@ -17,14 +17,14 @@ limitations under the License.
|
||||||
|
|
||||||
import Analytics from '../Analytics';
|
import Analytics from '../Analytics';
|
||||||
import { asyncAction } from './actionCreators';
|
import { asyncAction } from './actionCreators';
|
||||||
import TagOrderStore from '../stores/TagOrderStore';
|
import GroupFilterOrderStore from '../stores/GroupFilterOrderStore';
|
||||||
import { AsyncActionPayload } from "../dispatcher/payloads";
|
import { AsyncActionPayload } from "../dispatcher/payloads";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
|
|
||||||
export default class TagOrderActions {
|
export default class TagOrderActions {
|
||||||
/**
|
/**
|
||||||
* Creates an action thunk that will do an asynchronous request to
|
* 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
|
* @param {MatrixClient} matrixClient the matrix client to set the
|
||||||
* account data on.
|
* account data on.
|
||||||
|
@ -36,8 +36,8 @@ export default class TagOrderActions {
|
||||||
*/
|
*/
|
||||||
public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload {
|
public static moveTag(matrixClient: MatrixClient, tag: string, destinationIx: number): AsyncActionPayload {
|
||||||
// Only commit tags if the state is ready, i.e. not null
|
// Only commit tags if the state is ready, i.e. not null
|
||||||
let tags = TagOrderStore.getOrderedTags();
|
let tags = GroupFilterOrderStore.getOrderedTags();
|
||||||
let removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
let removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
|
||||||
if (!tags) {
|
if (!tags) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ export default class TagOrderActions {
|
||||||
|
|
||||||
removedTags = removedTags.filter((t) => t !== tag);
|
removedTags = removedTags.filter((t) => t !== tag);
|
||||||
|
|
||||||
const storeId = TagOrderStore.getStoreId();
|
const storeId = GroupFilterOrderStore.getStoreId();
|
||||||
|
|
||||||
return asyncAction('TagOrderActions.moveTag', () => {
|
return asyncAction('TagOrderActions.moveTag', () => {
|
||||||
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
Analytics.trackEvent('TagOrderActions', 'commitTagOrdering');
|
||||||
|
@ -83,8 +83,8 @@ export default class TagOrderActions {
|
||||||
*/
|
*/
|
||||||
public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload {
|
public static removeTag(matrixClient: MatrixClient, tag: string): AsyncActionPayload {
|
||||||
// Don't change tags, just removedTags
|
// Don't change tags, just removedTags
|
||||||
const tags = TagOrderStore.getOrderedTags();
|
const tags = GroupFilterOrderStore.getOrderedTags();
|
||||||
const removedTags = TagOrderStore.getRemovedTagsAccountData() || [];
|
const removedTags = GroupFilterOrderStore.getRemovedTagsAccountData() || [];
|
||||||
|
|
||||||
if (removedTags.includes(tag)) {
|
if (removedTags.includes(tag)) {
|
||||||
// Return a thunk that doesn't do anything, we don't even need
|
// Return a thunk that doesn't do anything, we don't even need
|
||||||
|
@ -94,7 +94,7 @@ export default class TagOrderActions {
|
||||||
|
|
||||||
removedTags.push(tag);
|
removedTags.push(tag);
|
||||||
|
|
||||||
const storeId = TagOrderStore.getStoreId();
|
const storeId = GroupFilterOrderStore.getStoreId();
|
||||||
|
|
||||||
return asyncAction('TagOrderActions.removeTag', () => {
|
return asyncAction('TagOrderActions.removeTag', () => {
|
||||||
Analytics.trackEvent('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 InlineSpinner from "../../../../components/views/elements/InlineSpinner";
|
||||||
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
|
||||||
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
|
import { getSecureBackupSetupMethods, isSecureBackupRequired } from '../../../../utils/WellKnownUtils';
|
||||||
|
import SecurityCustomisations from "../../../../customisations/Security";
|
||||||
|
|
||||||
const PHASE_LOADING = 0;
|
const PHASE_LOADING = 0;
|
||||||
const PHASE_LOADERROR = 1;
|
const PHASE_LOADERROR = 1;
|
||||||
|
@ -99,7 +100,8 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
|
|
||||||
this._passphraseField = createRef();
|
this._passphraseField = createRef();
|
||||||
|
|
||||||
this._fetchBackupInfo();
|
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
||||||
|
|
||||||
if (this.state.accountPassword) {
|
if (this.state.accountPassword) {
|
||||||
// If we have an account password in memory, let's simplify and
|
// If we have an account password in memory, let's simplify and
|
||||||
// assume it means password auth is also supported for device
|
// assume it means password auth is also supported for device
|
||||||
|
@ -110,13 +112,27 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
this._queryKeyUploadAuth();
|
this._queryKeyUploadAuth();
|
||||||
}
|
}
|
||||||
|
|
||||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
this._getInitialPhase();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatusChange);
|
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() {
|
async _fetchBackupInfo() {
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
@ -454,6 +470,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
value={CREATE_STORAGE_OPTION_KEY}
|
value={CREATE_STORAGE_OPTION_KEY}
|
||||||
name="keyPassphrase"
|
name="keyPassphrase"
|
||||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_KEY}
|
||||||
|
onChange={this._onKeyPassphraseChange}
|
||||||
outlined
|
outlined
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
|
@ -472,6 +489,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
value={CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||||
name="keyPassphrase"
|
name="keyPassphrase"
|
||||||
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
checked={this.state.passPhraseKeySelected === CREATE_STORAGE_OPTION_PASSPHRASE}
|
||||||
|
onChange={this._onKeyPassphraseChange}
|
||||||
outlined
|
outlined
|
||||||
>
|
>
|
||||||
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
<div className="mx_CreateSecretStorageDialog_optionTitle">
|
||||||
|
@ -493,7 +511,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
|
||||||
"Safeguard against losing access to encrypted messages & data by " +
|
"Safeguard against losing access to encrypted messages & data by " +
|
||||||
"backing up encryption keys on your server.",
|
"backing up encryption keys on your server.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup" onChange={this._onKeyPassphraseChange}>
|
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
|
||||||
{optionKey}
|
{optionKey}
|
||||||
{optionPassphrase}
|
{optionPassphrase}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -416,8 +416,9 @@ export const aboveLeftOf = (elementRect: DOMRect, chevronFace = ChevronFace.None
|
||||||
return menuOptions;
|
return menuOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useContextMenu = (): [boolean, RefObject<HTMLElement>, () => void, () => void, (val: boolean) => void] => {
|
type ContextMenuTuple<T> = [boolean, RefObject<T>, () => void, () => void, (val: boolean) => void];
|
||||||
const button = useRef<HTMLElement>(null);
|
export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<T> => {
|
||||||
|
const button = useRef<T>(null);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const open = () => {
|
const open = () => {
|
||||||
setIsOpen(true);
|
setIsOpen(true);
|
||||||
|
|
|
@ -16,7 +16,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import TagOrderStore from '../../stores/TagOrderStore';
|
import GroupFilterOrderStore from '../../stores/GroupFilterOrderStore';
|
||||||
|
|
||||||
import GroupActions from '../../actions/GroupActions';
|
import GroupActions from '../../actions/GroupActions';
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ import AutoHideScrollbar from "./AutoHideScrollbar";
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import UserTagTile from "../views/elements/UserTagTile";
|
import UserTagTile from "../views/elements/UserTagTile";
|
||||||
|
|
||||||
class TagPanel extends React.Component {
|
class GroupFilterPanel extends React.Component {
|
||||||
static contextType = MatrixClientContext;
|
static contextType = MatrixClientContext;
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
|
@ -44,13 +44,13 @@ class TagPanel extends React.Component {
|
||||||
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
this.context.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
this.context.on("sync", this._onClientSync);
|
this.context.on("sync", this._onClientSync);
|
||||||
|
|
||||||
this._tagOrderStoreToken = TagOrderStore.addListener(() => {
|
this._groupFilterOrderStoreToken = GroupFilterOrderStore.addListener(() => {
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
orderedTags: TagOrderStore.getOrderedTags() || [],
|
orderedTags: GroupFilterOrderStore.getOrderedTags() || [],
|
||||||
selectedTags: TagOrderStore.getSelectedTags(),
|
selectedTags: GroupFilterOrderStore.getSelectedTags(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// This could be done by anything with a matrix client
|
// This could be done by anything with a matrix client
|
||||||
|
@ -61,8 +61,8 @@ class TagPanel extends React.Component {
|
||||||
this.unmounted = true;
|
this.unmounted = true;
|
||||||
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
this.context.removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
this.context.removeListener("sync", this._onClientSync);
|
this.context.removeListener("sync", this._onClientSync);
|
||||||
if (this._tagOrderStoreToken) {
|
if (this._groupFilterOrderStoreToken) {
|
||||||
this._tagOrderStoreToken.remove();
|
this._groupFilterOrderStoreToken.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -98,7 +98,7 @@ class TagPanel extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<UserTagTile />
|
<UserTagTile />
|
||||||
<hr className="mx_TagPanel_divider" />
|
<hr className="mx_GroupFilterPanel_divider" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -117,8 +117,8 @@ class TagPanel extends React.Component {
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemsSelected = this.state.selectedTags.length > 0;
|
const itemsSelected = this.state.selectedTags.length > 0;
|
||||||
const classes = classNames('mx_TagPanel', {
|
const classes = classNames('mx_GroupFilterPanel', {
|
||||||
mx_TagPanel_items_selected: itemsSelected,
|
mx_GroupFilterPanel_items_selected: itemsSelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
let createButton = (
|
let createButton = (
|
||||||
|
@ -141,7 +141,7 @@ class TagPanel extends React.Component {
|
||||||
|
|
||||||
return <div className={classes} onClick={this.onClearFilterClick}>
|
return <div className={classes} onClick={this.onClearFilterClick}>
|
||||||
<AutoHideScrollbar
|
<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
|
// 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
|
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
|
||||||
onMouseDown={this.onMouseDown}
|
onMouseDown={this.onMouseDown}
|
||||||
|
@ -152,7 +152,7 @@ class TagPanel extends React.Component {
|
||||||
>
|
>
|
||||||
{ (provided, snapshot) => (
|
{ (provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className="mx_TagPanel_tagTileContainer"
|
className="mx_GroupFilterPanel_tagTileContainer"
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{ this.renderGlobalIcon() }
|
{ this.renderGlobalIcon() }
|
||||||
|
@ -168,4 +168,4 @@ class TagPanel extends React.Component {
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export default TagPanel;
|
export default GroupFilterPanel;
|
|
@ -620,7 +620,7 @@ export default class GroupView extends React.Component {
|
||||||
profileForm: newProfileForm,
|
profileForm: newProfileForm,
|
||||||
|
|
||||||
// Indicate that FlairStore needs to be poked to show this change
|
// 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,
|
avatarChanged: true,
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
|
@ -649,7 +649,6 @@ export default class GroupView extends React.Component {
|
||||||
editing: false,
|
editing: false,
|
||||||
summary: null,
|
summary: null,
|
||||||
});
|
});
|
||||||
dis.dispatch({action: 'panel_disable'});
|
|
||||||
this._initGroupStore(this.props.groupId);
|
this._initGroupStore(this.props.groupId);
|
||||||
|
|
||||||
if (this.state.avatarChanged) {
|
if (this.state.avatarChanged) {
|
||||||
|
@ -870,10 +869,7 @@ export default class GroupView extends React.Component {
|
||||||
{ _t('Add rooms to this community') }
|
{ _t('Add rooms to this community') }
|
||||||
</div>
|
</div>
|
||||||
</AccessibleButton>) : <div />;
|
</AccessibleButton>) : <div />;
|
||||||
const roomDetailListClassName = classnames({
|
|
||||||
"mx_fadable": true,
|
|
||||||
"mx_fadable_faded": this.state.editing,
|
|
||||||
});
|
|
||||||
return <div className="mx_GroupView_rooms">
|
return <div className="mx_GroupView_rooms">
|
||||||
<div className="mx_GroupView_rooms_header">
|
<div className="mx_GroupView_rooms_header">
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -884,9 +880,7 @@ export default class GroupView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
{ this.state.groupRoomsLoading ?
|
{ this.state.groupRoomsLoading ?
|
||||||
<Spinner /> :
|
<Spinner /> :
|
||||||
<RoomDetailList
|
<RoomDetailList rooms={this.state.groupRooms} />
|
||||||
rooms={this.state.groupRooms}
|
|
||||||
className={roomDetailListClassName} />
|
|
||||||
}
|
}
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|