Merge remote-tracking branch 'origin' into posthog-analytics

This commit is contained in:
James Salter 2021-07-28 11:13:22 +01:00
commit bd7e2dee3d
487 changed files with 11710 additions and 6874 deletions

View File

@ -1,3 +1,152 @@
Changes in [3.26.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0) (2021-07-19)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.26.0-rc.1...v3.26.0)
* Fix 'User' type import
[\#6376](https://github.com/matrix-org/matrix-react-sdk/pull/6376)
Changes in [3.26.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.26.0-rc.1) (2021-07-14)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0...v3.26.0-rc.1)
* Fix voice messages in right panels
[\#6370](https://github.com/matrix-org/matrix-react-sdk/pull/6370)
* Use TileShape enum more universally
[\#6369](https://github.com/matrix-org/matrix-react-sdk/pull/6369)
* Translations update from Weblate
[\#6373](https://github.com/matrix-org/matrix-react-sdk/pull/6373)
* Hide world readable history option in encrypted rooms
[\#5947](https://github.com/matrix-org/matrix-react-sdk/pull/5947)
* Make the Image View buttons easier to hit
[\#6372](https://github.com/matrix-org/matrix-react-sdk/pull/6372)
* Reorder buttons in the Image View
[\#6368](https://github.com/matrix-org/matrix-react-sdk/pull/6368)
* Add VS Code to gitignore
[\#6367](https://github.com/matrix-org/matrix-react-sdk/pull/6367)
* Fix inviter exploding due to member being null
[\#6362](https://github.com/matrix-org/matrix-react-sdk/pull/6362)
* Increase sample count in voice message thumbnail
[\#6359](https://github.com/matrix-org/matrix-react-sdk/pull/6359)
* Improve arraySeed utility
[\#6360](https://github.com/matrix-org/matrix-react-sdk/pull/6360)
* Convert FontManager to TS and stub it out for tests
[\#6358](https://github.com/matrix-org/matrix-react-sdk/pull/6358)
* Adjust recording waveform behaviour for voice messages
[\#6357](https://github.com/matrix-org/matrix-react-sdk/pull/6357)
* Do not honor string power levels
[\#6245](https://github.com/matrix-org/matrix-react-sdk/pull/6245)
* Add alias and directory customisation points
[\#6343](https://github.com/matrix-org/matrix-react-sdk/pull/6343)
* Fix multiinviter user already in room and clean up code
[\#6354](https://github.com/matrix-org/matrix-react-sdk/pull/6354)
* Fix right panel not closing user info when changing rooms
[\#6341](https://github.com/matrix-org/matrix-react-sdk/pull/6341)
* Quit sticker picker on m.sticker
[\#5679](https://github.com/matrix-org/matrix-react-sdk/pull/5679)
* Don't autodetect language in inline code blocks
[\#6350](https://github.com/matrix-org/matrix-react-sdk/pull/6350)
* Make ghost button background transparent
[\#6331](https://github.com/matrix-org/matrix-react-sdk/pull/6331)
* only consider valid & loaded url previews for show N more prompt
[\#6346](https://github.com/matrix-org/matrix-react-sdk/pull/6346)
* Extract MXCs from _matrix/media/r0/ URLs for inline images in messages
[\#6335](https://github.com/matrix-org/matrix-react-sdk/pull/6335)
* Fix small visual regression with the site name on url previews
[\#6342](https://github.com/matrix-org/matrix-react-sdk/pull/6342)
* Make PIP CallView draggable/movable
[\#5952](https://github.com/matrix-org/matrix-react-sdk/pull/5952)
* Convert VoiceUserSettingsTab to TS
[\#6340](https://github.com/matrix-org/matrix-react-sdk/pull/6340)
* Simplify typescript definition for Modernizr
[\#6339](https://github.com/matrix-org/matrix-react-sdk/pull/6339)
* Remember the last used server for room directory searches
[\#6322](https://github.com/matrix-org/matrix-react-sdk/pull/6322)
* Focus composer after reacting
[\#6332](https://github.com/matrix-org/matrix-react-sdk/pull/6332)
* Fix bug which prevented more than one event getting pinned
[\#6336](https://github.com/matrix-org/matrix-react-sdk/pull/6336)
* Make DeviceListener also update on megolm key in SSSS
[\#6337](https://github.com/matrix-org/matrix-react-sdk/pull/6337)
* Improve URL previews
[\#6326](https://github.com/matrix-org/matrix-react-sdk/pull/6326)
* Don't close settings dialog when opening spaces feedback prompt
[\#6334](https://github.com/matrix-org/matrix-react-sdk/pull/6334)
* Update import location for types
[\#6330](https://github.com/matrix-org/matrix-react-sdk/pull/6330)
* Improve blurhash rendering performance
[\#6329](https://github.com/matrix-org/matrix-react-sdk/pull/6329)
* Use a proper color scheme for codeblocks
[\#6320](https://github.com/matrix-org/matrix-react-sdk/pull/6320)
* Burn `sdk.getComponent()` with 🔥
[\#6308](https://github.com/matrix-org/matrix-react-sdk/pull/6308)
* Fix instances of the Edit Message Composer's save button being wrongly
disabled
[\#6307](https://github.com/matrix-org/matrix-react-sdk/pull/6307)
* Do not generate a lockfile when running in CI
[\#6327](https://github.com/matrix-org/matrix-react-sdk/pull/6327)
* Update lockfile with correct dependencies
[\#6324](https://github.com/matrix-org/matrix-react-sdk/pull/6324)
* Clarify the keys we use when submitting rageshakes
[\#6321](https://github.com/matrix-org/matrix-react-sdk/pull/6321)
* Fix ImageView context menu
[\#6318](https://github.com/matrix-org/matrix-react-sdk/pull/6318)
* TypeScript migration
[\#6315](https://github.com/matrix-org/matrix-react-sdk/pull/6315)
* Move animation to compositor
[\#6310](https://github.com/matrix-org/matrix-react-sdk/pull/6310)
* Reorganize preferences
[\#5742](https://github.com/matrix-org/matrix-react-sdk/pull/5742)
* Fix being able to un-rotate images
[\#6313](https://github.com/matrix-org/matrix-react-sdk/pull/6313)
* Fix icon size in passphrase prompt
[\#6312](https://github.com/matrix-org/matrix-react-sdk/pull/6312)
* Use sleep & defer from js-sdk instead of duplicating it
[\#6305](https://github.com/matrix-org/matrix-react-sdk/pull/6305)
* Convert EventTimeline, EventTimelineSet and TimelineWindow to TS
[\#6295](https://github.com/matrix-org/matrix-react-sdk/pull/6295)
* Comply with new member-delimiter-style rule
[\#6306](https://github.com/matrix-org/matrix-react-sdk/pull/6306)
* Fix Test Linting
[\#6304](https://github.com/matrix-org/matrix-react-sdk/pull/6304)
* Convert Markdown to TypeScript
[\#6303](https://github.com/matrix-org/matrix-react-sdk/pull/6303)
* Convert RoomHeader to TS
[\#6302](https://github.com/matrix-org/matrix-react-sdk/pull/6302)
* Prevent RoomDirectory from exploding when filterString is wrongly nulled
[\#6296](https://github.com/matrix-org/matrix-react-sdk/pull/6296)
* Add support for blurhash (MSC2448)
[\#5099](https://github.com/matrix-org/matrix-react-sdk/pull/5099)
* Remove rateLimitedFunc
[\#6300](https://github.com/matrix-org/matrix-react-sdk/pull/6300)
* Convert some Key Verification classes to TypeScript
[\#6299](https://github.com/matrix-org/matrix-react-sdk/pull/6299)
* Typescript conversion of Composer components and more
[\#6292](https://github.com/matrix-org/matrix-react-sdk/pull/6292)
* Upgrade browserlist target versions
[\#6298](https://github.com/matrix-org/matrix-react-sdk/pull/6298)
* Fix browser crashing when searching for a malformed HTML tag
[\#6297](https://github.com/matrix-org/matrix-react-sdk/pull/6297)
* Add custom audio player
[\#6264](https://github.com/matrix-org/matrix-react-sdk/pull/6264)
* Lint MXC APIs to centralise access
[\#6293](https://github.com/matrix-org/matrix-react-sdk/pull/6293)
* Remove reminescent references to the tinter
[\#6290](https://github.com/matrix-org/matrix-react-sdk/pull/6290)
* More js-sdk type consolidation
[\#6263](https://github.com/matrix-org/matrix-react-sdk/pull/6263)
* Convert MessagePanel, TimelinePanel, ScrollPanel, and more to Typescript
[\#6243](https://github.com/matrix-org/matrix-react-sdk/pull/6243)
* Migrate to `eslint-plugin-matrix-org`
[\#6285](https://github.com/matrix-org/matrix-react-sdk/pull/6285)
* Avoid cyclic dependencies by moving watchers out of constructor
[\#6287](https://github.com/matrix-org/matrix-react-sdk/pull/6287)
* Add spacing between toast buttons with cross browser support in mind
[\#6284](https://github.com/matrix-org/matrix-react-sdk/pull/6284)
* Deprecate Tinter and TintableSVG
[\#6279](https://github.com/matrix-org/matrix-react-sdk/pull/6279)
* Migrate FilePanel to TypeScript
[\#6283](https://github.com/matrix-org/matrix-react-sdk/pull/6283)
Changes in [3.25.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.25.0) (2021-07-05)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.25.0-rc.1...v3.25.0)

View File

@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
"version": "3.25.0",
"version": "3.26.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@ -46,6 +46,7 @@
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 src test",
"lint:js-fix": "eslint --fix src test",
"lint:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest",
@ -64,8 +65,8 @@
"counterpart": "^0.18.6",
"diff-dom": "^4.2.2",
"diff-match-patch": "^1.0.5",
"emojibase-data": "^5.1.1",
"emojibase-regex": "^4.1.1",
"emojibase-data": "^6.2.0",
"emojibase-regex": "^5.1.3",
"escape-html": "^1.0.3",
"file-saver": "^2.0.5",
"filesize": "6.1.0",
@ -79,7 +80,7 @@
"katex": "^0.12.0",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.20",
"matrix-js-sdk": "12.0.1",
"matrix-js-sdk": "12.1.0",
"matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5",
"opus-recorder": "^8.0.3",

View File

@ -104,8 +104,8 @@ a:visited {
input[type=text],
input[type=search],
input[type=password] {
font-family: inherit;
padding: 9px;
font-family: $font-family;
font-size: $font-14px;
font-weight: 600;
min-width: 0;
@ -146,7 +146,6 @@ input[type=text], input[type=password], textarea {
/* Required by Firefox */
textarea {
font-family: $font-family;
color: $primary-fg-color;
}

View File

@ -86,6 +86,7 @@
@import "./views/dialogs/_IncomingSasDialog.scss";
@import "./views/dialogs/_InviteDialog.scss";
@import "./views/dialogs/_KeyboardShortcutsDialog.scss";
@import "./views/dialogs/_ManageRestrictedJoinRuleDialog.scss";
@import "./views/dialogs/_MessageEditHistoryDialog.scss";
@import "./views/dialogs/_ModalWidgetDialog.scss";
@import "./views/dialogs/_NewSessionReviewDialog.scss";
@ -159,6 +160,7 @@
@import "./views/groups/_GroupPublicityToggle.scss";
@import "./views/groups/_GroupRoomList.scss";
@import "./views/groups/_GroupUserSettings.scss";
@import "./views/messages/_CallEvent.scss";
@import "./views/messages/_CreateEvent.scss";
@import "./views/messages/_DateSeparator.scss";
@import "./views/messages/_EventTileBubble.scss";
@ -171,7 +173,6 @@
@import "./views/messages/_MStickerBody.scss";
@import "./views/messages/_MTextBody.scss";
@import "./views/messages/_MVideoBody.scss";
@import "./views/messages/_MVoiceMessageBody.scss";
@import "./views/messages/_MediaBody.scss";
@import "./views/messages/_MessageActionBar.scss";
@import "./views/messages/_MessageTimestamp.scss";
@ -200,6 +201,7 @@
@import "./views/rooms/_E2EIcon.scss";
@import "./views/rooms/_EditMessageComposer.scss";
@import "./views/rooms/_EntityTile.scss";
@import "./views/rooms/_EventBubbleTile.scss";
@import "./views/rooms/_EventTile.scss";
@import "./views/rooms/_GroupLayout.scss";
@import "./views/rooms/_IRCLayout.scss";
@ -267,6 +269,7 @@
@import "./views/voip/_CallPreview.scss";
@import "./views/voip/_CallView.scss";
@import "./views/voip/_CallViewForRoom.scss";
@import "./views/voip/_CallViewSidebar.scss";
@import "./views/voip/_DialPad.scss";
@import "./views/voip/_DialPadContextMenu.scss";
@import "./views/voip/_DialPadModal.scss";

View File

@ -45,9 +45,14 @@ limitations under the License.
/* Overrides for the attachment body tiles */
.mx_FilePanel .mx_EventTile {
.mx_FilePanel .mx_EventTile:not([data-layout=bubble]) {
word-break: break-word;
margin-top: 32px;
margin-top: 10px;
padding-top: 0;
.mx_EventTile_line {
padding-left: 0;
}
}
.mx_FilePanel .mx_EventTile .mx_MImageBody {
@ -118,10 +123,6 @@ limitations under the License.
padding-left: 0px;
}
.mx_FilePanel .mx_EventTile:hover .mx_EventTile_line {
background-color: $primary-bg-color;
}
.mx_FilePanel_empty::before {
mask-image: url('$(res)/img/element-icons/room/files.svg');
}

View File

@ -84,7 +84,7 @@ limitations under the License.
display: inline;
}
.mx_NotificationPanel .mx_EventTile_senderDetails {
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_senderDetails {
padding-left: 36px; // align with the room name
position: relative;
@ -105,7 +105,7 @@ limitations under the License.
padding-left: 5px;
}
.mx_NotificationPanel .mx_EventTile_line {
.mx_NotificationPanel .mx_EventTile:not([data-layout=bubble]) .mx_EventTile_line {
margin-right: 0px;
padding-left: 36px; // align with the room name
padding-top: 0px;

View File

@ -190,7 +190,6 @@ limitations under the License.
position: relative;
padding: 8px 16px;
border-radius: 8px;
min-height: 56px;
box-sizing: border-box;
display: grid;

View File

@ -234,6 +234,9 @@ $SpaceRoomViewInnerWidth: 428px;
}
.mx_SpaceRoomView_landing {
display: flex;
flex-direction: column;
> .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px;
@ -340,6 +343,7 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SearchBox {
margin: 0 0 20px;
flex: 0;
}
.mx_SpaceFeedbackPrompt {
@ -350,6 +354,11 @@ $SpaceRoomViewInnerWidth: 428px;
display: none;
}
}
.mx_SpaceRoomDirectory_list {
// we don't want this container to get forced into the flexbox layout
display: contents;
}
}
.mx_SpaceRoomView_privateScope {

View File

@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_AudioPlayer_container {
.mx_MediaBody.mx_AudioPlayer_container {
padding: 16px 12px 12px 12px;
max-width: 267px; // use max to make the control fit in the files/pinned panels
.mx_AudioPlayer_primaryContainer {
display: flex;

View File

@ -18,10 +18,10 @@ limitations under the License.
// are shared amongst multiple voice message components.
// Container for live recording and playback controls
.mx_VoiceMessagePrimaryContainer {
// 7px top and bottom for visual design. 12px left & right, but the waveform (right)
// has a 1px padding on it that we want to account for.
padding: 7px 12px 7px 11px;
.mx_MediaBody.mx_VoiceMessagePrimaryContainer {
// The waveform (right) has a 1px padding on it that we want to account for, otherwise
// inherit from mx_MediaBody
padding-right: 11px;
// Cheat at alignment a bit
display: flex;

View File

@ -29,7 +29,6 @@ limitations under the License.
.mx_AddressPickerDialog_input:focus {
height: 26px;
font-size: $font-14px;
font-family: $font-family;
padding-left: 12px;
padding-right: 12px;
margin: 0 !important;

View File

@ -34,7 +34,6 @@ limitations under the License.
}
.mx_ConfirmUserActionDialog_reasonField {
font-family: $font-family;
font-size: $font-14px;
color: $primary-fg-color;
background-color: $primary-bg-color;

View File

@ -65,7 +65,7 @@ limitations under the License.
.mx_CreateRoomDialog_aliasContainer {
display: flex;
// put margin on container so it can collapse with siblings
margin: 10px 0;
margin: 24px 0 10px;
.mx_RoomAliasField {
margin: 0;
@ -101,10 +101,6 @@ limitations under the License.
margin-left: 30px;
}
.mx_CreateRoomDialog_topic {
margin-bottom: 36px;
}
.mx_Dialog_content > .mx_SettingsFlag {
margin-top: 24px;
}
@ -113,5 +109,56 @@ limitations under the License.
margin: 0 85px 0 0;
font-size: $font-12px;
}
.mx_Dropdown {
margin-bottom: 8px;
font-weight: normal;
font-family: $font-family;
font-size: $font-14px;
color: $primary-fg-color;
.mx_Dropdown_input {
border: 1px solid $input-border-color;
}
.mx_Dropdown_option {
font-size: $font-14px;
line-height: $font-32px;
height: 32px;
min-height: 32px;
> div {
padding-left: 30px;
position: relative;
&::before {
content: "";
position: absolute;
height: 16px;
width: 16px;
left: 6px;
top: 8px;
mask-repeat: no-repeat;
mask-position: center;
background-color: $secondary-fg-color;
}
}
}
.mx_CreateRoomDialog_dropdown_invite::before {
mask-image: url('$(res)/img/element-icons/lock.svg');
mask-size: contain;
}
.mx_CreateRoomDialog_dropdown_public::before {
mask-image: url('$(res)/img/globe.svg');
mask-size: 12px;
}
.mx_CreateRoomDialog_dropdown_restricted::before {
mask-image: url('$(res)/img/element-icons/community-members.svg');
mask-size: contain;
}
}
}

View File

@ -55,22 +55,6 @@ limitations under the License.
padding-right: 24px;
}
.mx_DevTools_inputCell {
display: table-cell;
width: 240px;
}
.mx_DevTools_inputCell input {
display: inline-block;
border: 0;
border-bottom: 1px solid $input-underline-color;
padding: 0;
width: 240px;
color: $input-fg-color;
font-family: $font-family;
font-size: $font-16px;
}
.mx_DevTools_textarea {
font-size: $font-12px;
max-width: 684px;
@ -139,7 +123,6 @@ limitations under the License.
+ .mx_DevTools_tgl-btn {
padding: 2px;
transition: all .2s ease;
font-family: sans-serif;
perspective: 100px;
&::after,
&::before {

View File

@ -36,6 +36,10 @@ limitations under the License.
flex-shrink: 0;
overflow-y: auto;
.mx_EventTile[data-layout=bubble] {
margin-top: 20px;
}
div {
pointer-events: none;
}

View File

@ -0,0 +1,150 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_ManageRestrictedJoinRuleDialog_wrapper {
.mx_Dialog {
display: flex;
flex-direction: column;
}
}
.mx_ManageRestrictedJoinRuleDialog {
width: 480px;
color: $primary-fg-color;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
min-height: 0;
height: 60vh;
.mx_SearchBox {
// To match the space around the title
margin: 0 0 15px 0;
flex-grow: 0;
}
.mx_ManageRestrictedJoinRuleDialog_content {
flex-grow: 1;
}
.mx_ManageRestrictedJoinRuleDialog_noResults {
display: block;
margin-top: 24px;
}
.mx_ManageRestrictedJoinRuleDialog_section {
&:not(:first-child) {
margin-top: 24px;
}
> h3 {
margin: 0;
color: $secondary-fg-color;
font-size: $font-12px;
font-weight: $font-semi-bold;
line-height: $font-15px;
}
.mx_ManageRestrictedJoinRuleDialog_entry {
display: flex;
margin-top: 12px;
> div {
flex-grow: 1;
}
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 4px;
}
.mx_ManageRestrictedJoinRuleDialog_entry_name {
margin: 0 8px;
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.mx_ManageRestrictedJoinRuleDialog_entry_description {
margin-top: 8px;
font-size: $font-12px;
line-height: $font-15px;
color: $tertiary-fg-color;
}
.mx_Checkbox {
align-items: center;
}
}
}
.mx_ManageRestrictedJoinRuleDialog_section_spaces {
.mx_BaseAvatar {
margin-right: 12px;
}
.mx_BaseAvatar_image {
border-radius: 8px;
}
}
.mx_ManageRestrictedJoinRuleDialog_section_info {
position: relative;
border-radius: 8px;
margin: 12px 0;
padding: 8px 8px 8px 42px;
background-color: $header-panel-bg-color;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
&::before {
content: '';
position: absolute;
left: 10px;
top: calc(50% - 8px); // vertical centering
height: 16px;
width: 16px;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-image: url('$(res)/img/element-icons/room/room-summary.svg');
mask-position: center;
}
}
.mx_ManageRestrictedJoinRuleDialog_footer {
margin-top: 20px;
.mx_ManageRestrictedJoinRuleDialog_footer_buttons {
display: flex;
width: max-content;
margin-left: auto;
.mx_AccessibleButton {
display: inline-block;
& + .mx_AccessibleButton {
margin-left: 24px;
}
}
}
}
}

View File

@ -16,57 +16,43 @@ limitations under the License.
.mx_desktopCapturerSourcePicker {
overflow: hidden;
}
.mx_desktopCapturerSourcePicker_tabLabels {
display: flex;
padding: 0 0 8px 0;
}
.mx_desktopCapturerSourcePicker_tab {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
height: 500px;
overflow: overlay;
}
.mx_desktopCapturerSourcePicker_tabLabel,
.mx_desktopCapturerSourcePicker_tabLabel_selected {
width: 100%;
text-align: center;
border-radius: 8px;
padding: 8px 0;
font-size: $font-13px;
}
.mx_desktopCapturerSourcePicker_source {
display: flex;
flex-direction: column;
margin: 8px;
}
.mx_desktopCapturerSourcePicker_tabLabel_selected {
background-color: $tab-label-active-bg-color;
color: $tab-label-active-fg-color;
}
.mx_desktopCapturerSourcePicker_source_thumbnail {
margin: 4px;
padding: 4px;
width: 312px;
border-width: 2px;
border-radius: 8px;
border-style: solid;
border-color: transparent;
.mx_desktopCapturerSourcePicker_panel {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: flex-start;
height: 500px;
overflow: overlay;
}
&.mx_desktopCapturerSourcePicker_source_thumbnail_selected,
&:hover,
&:focus {
border-color: $accent-color;
}
}
.mx_desktopCapturerSourcePicker_stream_button {
display: flex;
flex-direction: column;
margin: 8px;
border-radius: 4px;
}
.mx_desktopCapturerSourcePicker_stream_button:hover,
.mx_desktopCapturerSourcePicker_stream_button:focus {
background: $roomtile-selected-bg-color;
}
.mx_desktopCapturerSourcePicker_stream_thumbnail {
margin: 4px;
width: 312px;
}
.mx_desktopCapturerSourcePicker_stream_name {
margin: 0 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 312px;
.mx_desktopCapturerSourcePicker_source_name {
margin: 0 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 312px;
}
}

View File

@ -27,7 +27,7 @@ limitations under the License.
display: flex;
align-items: center;
position: relative;
border-radius: 3px;
border-radius: 4px;
border: 1px solid $strong-input-border-color;
font-size: $font-12px;
user-select: none;
@ -109,7 +109,7 @@ input.mx_Dropdown_option:focus {
z-index: 2;
margin: 0;
padding: 0px;
border-radius: 3px;
border-radius: 4px;
border: 1px solid $input-focused-border-color;
background-color: $primary-bg-color;
max-height: 200px;

View File

@ -39,7 +39,6 @@ limitations under the License.
.mx_Field select,
.mx_Field textarea {
font-weight: normal;
font-family: $font-family;
font-size: $font-14px;
border: none;
// Even without a border here, we still need this avoid overlapping the rounded

View File

@ -30,5 +30,12 @@ limitations under the License.
mask-position: center;
content: '';
vertical-align: middle;
}
.mx_InfoTooltip_icon_info::before {
mask-image: url('$(res)/img/element-icons/info.svg');
}
.mx_InfoTooltip_icon_warning::before {
mask-image: url('$(res)/img/element-icons/warning.svg');
}

View File

@ -19,8 +19,9 @@ limitations under the License.
margin-left: 0;
margin-right: 0;
margin-bottom: 8px;
padding-left: 10px;
border-left: 4px solid $button-bg-color;
padding: 0 10px;
border-left: 2px solid $button-bg-color;
border-radius: 2px;
.mx_ReplyThread_show {
cursor: pointer;

View File

@ -0,0 +1,162 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CallEvent {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background-color: $dark-panel-bg-color;
border-radius: 8px;
margin: 10px auto;
max-width: 75%;
box-sizing: border-box;
height: 60px;
&.mx_CallEvent_voice {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}
&.mx_CallEvent_video {
.mx_CallEvent_type_icon::before,
.mx_CallEvent_content_button_callBack span::before,
.mx_CallEvent_content_button_answer span::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
}
&.mx_CallEvent_voice.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-voice.svg');
}
&.mx_CallEvent_video.mx_CallEvent_missed .mx_CallEvent_type_icon::before {
mask-image: url('$(res)/img/voip/missed-video.svg');
}
.mx_CallEvent_info {
display: flex;
flex-direction: row;
align-items: center;
margin-left: 12px;
.mx_CallEvent_info_basic {
display: flex;
flex-direction: column;
margin-left: 10px; // To match mx_CallEvent
.mx_CallEvent_sender {
font-weight: 600;
font-size: 1.5rem;
line-height: 1.8rem;
margin-bottom: 3px;
}
.mx_CallEvent_type {
font-weight: 400;
color: $secondary-fg-color;
font-size: 1.2rem;
line-height: $font-13px;
display: flex;
align-items: center;
.mx_CallEvent_type_icon {
height: 13px;
width: 13px;
margin-right: 5px;
&::before {
content: '';
position: absolute;
height: 13px;
width: 13px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
}
}
}
}
}
.mx_CallEvent_content {
display: flex;
flex-direction: row;
align-items: center;
color: $secondary-fg-color;
margin-right: 16px;
.mx_CallEvent_content_button {
height: 24px;
padding: 0px 12px;
margin-left: 8px;
span {
padding: 8px 0;
display: flex;
align-items: center;
&::before {
content: '';
display: inline-block;
background-color: $button-fg-color;
mask-position: center;
mask-repeat: no-repeat;
mask-size: 16px;
width: 16px;
height: 16px;
margin-right: 8px;
}
}
}
.mx_CallEvent_content_button_reject span::before {
mask-image: url('$(res)/img/element-icons/call/hangup.svg');
}
.mx_CallEvent_content_tooltip {
margin-right: 5px;
}
.mx_CallEvent_iconButton {
display: inline-flex;
margin-right: 8px;
&::before {
content: '';
height: 16px;
width: 16px;
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.mx_CallEvent_silence::before {
mask-image: url('$(res)/img/voip/silence.svg');
}
.mx_CallEvent_unSilence::before {
mask-image: url('$(res)/img/voip/un-silence.svg');
}
}
}

View File

@ -60,12 +60,6 @@ limitations under the License.
}
.mx_MFileBody_info {
background-color: $message-body-panel-bg-color;
border-radius: 12px;
width: 243px; // same width as a playable voice message, accounting for padding
padding: 6px 12px;
color: $message-body-panel-fg-color;
.mx_MFileBody_info_icon {
background-color: $message-body-panel-icon-bg-color;
border-radius: 20px;

View File

@ -16,20 +16,15 @@ limitations under the License.
$timelineImageBorderRadius: 4px;
.mx_MImageBody {
display: block;
margin-right: 34px;
}
.mx_MImageBody_thumbnail {
position: absolute;
width: 100%;
height: 100%;
left: 0;
top: 0;
object-fit: contain;
border-radius: $timelineImageBorderRadius;
> canvas {
display: flex;
justify-content: center;
align-items: center;
> div > canvas {
border-radius: $timelineImageBorderRadius;
}
}
@ -43,17 +38,6 @@ $timelineImageBorderRadius: 4px;
position: relative;
}
.mx_MImageBody_thumbnail_spinner {
position: absolute;
left: 50%;
top: 50%;
}
// Inner img should be centered around 0, 0
.mx_MImageBody_thumbnail_spinner > * {
transform: translate(-50%, -50%);
}
.mx_MImageBody_gifLabel {
position: absolute;
display: block;

View File

@ -20,9 +20,11 @@ limitations under the License.
.mx_MediaBody {
background-color: $message-body-panel-bg-color;
border-radius: 12px;
max-width: 243px; // use max-width instead of width so it fits within right panels
color: $message-body-panel-fg-color;
font-size: $font-14px;
line-height: $font-24px;
}
padding: 6px 12px;
}

View File

@ -107,3 +107,12 @@ limitations under the License.
.mx_MessageActionBar_cancelButton::after {
mask-image: url('$(res)/img/element-icons/trashcan.svg');
}
.mx_MessageActionBar_downloadButton::after {
mask-size: 14px;
mask-image: url('$(res)/img/download.svg');
}
.mx_MessageActionBar_downloadButton.mx_MessageActionBar_downloadSpinnerButton::after {
background-color: transparent; // hide the download icon mask
}

View File

@ -26,6 +26,7 @@ limitations under the License.
height: 24px;
vertical-align: middle;
margin-left: 4px;
margin-right: 4px;
&::before {
content: '';

View File

@ -0,0 +1,335 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_EventTile[data-layout=bubble],
.mx_EventListSummary[data-layout=bubble] {
--avatarSize: 32px;
--gutterSize: 11px;
--cornerRadius: 12px;
--maxWidth: 70%;
}
.mx_EventTile[data-layout=bubble] {
position: relative;
margin-top: var(--gutterSize);
margin-left: 50px;
margin-right: 100px;
&.mx_EventTile_continuation {
margin-top: 2px;
}
/* For replies */
.mx_EventTile {
padding-top: 0;
}
&:hover,
&.mx_EventTile_selected {
&::before {
content: '';
position: absolute;
top: -1px;
bottom: -1px;
left: -60px;
right: -60px;
z-index: -1;
background: $eventbubble-bg-hover;
border-radius: 4px;
}
.mx_EventTile_avatar {
img {
box-shadow: 0 0 0 3px $eventbubble-bg-hover;
}
}
}
.mx_SenderProfile,
.mx_EventTile_line {
width: fit-content;
max-width: 70%;
}
.mx_SenderProfile {
position: relative;
top: -2px;
left: 2px;
}
&[data-self=false] {
.mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius);
}
.mx_EventTile_avatar {
left: -34px;
}
.mx_MessageActionBar {
right: 0;
transform: translate3d(90%, 50%, 0);
}
--backgroundColor: $eventbubble-others-bg;
}
&[data-self=true] {
.mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
float: right;
> a {
left: auto;
right: -68px;
}
}
.mx_SenderProfile {
display: none;
}
.mx_ReplyTile .mx_SenderProfile {
display: block;
}
.mx_ReactionsRow {
float: right;
clear: right;
display: flex;
/* Moving the "add reaction button" before the reactions */
> :last-child {
order: -1;
}
}
.mx_EventTile_avatar {
top: -19px; // height of the sender block
right: -35px;
}
--backgroundColor: $eventbubble-self-bg;
}
.mx_EventTile_line {
position: relative;
padding: var(--gutterSize);
border-top-left-radius: var(--cornerRadius);
border-top-right-radius: var(--cornerRadius);
background: var(--backgroundColor);
display: flex;
gap: 5px;
margin: 0 -12px 0 -9px;
> a {
position: absolute;
padding: 10px 20px;
top: 0;
left: -68px;
}
}
&.mx_EventTile_continuation[data-self=false] .mx_EventTile_line {
border-top-left-radius: 0;
}
&.mx_EventTile_lastInSection[data-self=false] .mx_EventTile_line {
border-bottom-left-radius: var(--cornerRadius);
}
&.mx_EventTile_continuation[data-self=true] .mx_EventTile_line {
border-top-right-radius: 0;
}
&.mx_EventTile_lastInSection[data-self=true] .mx_EventTile_line {
border-bottom-right-radius: var(--cornerRadius);
}
.mx_EventTile_avatar {
position: absolute;
top: 0;
line-height: 1;
z-index: 9;
img {
box-shadow: 0 0 0 3px $eventbubble-avatar-outline;
border-radius: 50%;
}
}
&.mx_EventTile_noSender {
.mx_EventTile_avatar {
top: -19px;
}
}
.mx_BaseAvatar,
.mx_EventTile_avatar {
line-height: 1;
}
&[data-has-reply=true] {
> .mx_EventTile_line {
flex-direction: column;
}
.mx_ReplyThread_show {
order: 99999;
}
.mx_ReplyThread {
margin: 0 calc(-1 * var(--gutterSize));
.mx_EventTile_reply {
max-width: 90%;
padding: 0;
> a {
display: none !important;
}
}
.mx_EventTile {
display: flex;
gap: var(--gutterSize);
.mx_EventTile_avatar {
position: static;
}
.mx_SenderProfile {
display: none;
}
}
}
}
.mx_EditMessageComposer_buttons {
position: static;
padding: 0;
margin: 0;
background: transparent;
}
.mx_ReactionsRow {
margin-right: -18px;
margin-left: -9px;
}
.mx_ReplyThread {
border-left-width: 2px;
border-left-color: $eventbubble-reply-color;
}
/* Special layout scenario for "Unable To Decrypt (UTD)" events */
&.mx_EventTile_bad > .mx_EventTile_line {
display: grid;
grid-template:
"reply reply" auto
"shield body" auto
"shield link" auto
/ auto 1fr;
.mx_EventTile_e2eIcon {
grid-area: shield;
}
.mx_UnknownBody {
grid-area: body;
}
.mx_EventTile_keyRequestInfo {
grid-area: link;
}
.mx_ReplyThread_wrapper {
grid-area: reply;
}
}
.mx_EventTile_readAvatars {
position: absolute;
right: -110px;
bottom: 0;
top: auto;
}
.mx_MTextBody {
max-width: 100%;
}
}
.mx_EventTile.mx_EventTile_bubbleContainer[data-layout=bubble],
.mx_EventTile.mx_EventTile_info[data-layout=bubble],
.mx_EventListSummary[data-layout=bubble][data-expanded=false] {
--backgroundColor: transparent;
--gutterSize: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 5px 0;
.mx_EventTile_avatar {
position: static;
order: -1;
margin-right: 5px;
}
}
.mx_EventListSummary[data-layout=bubble] {
--maxWidth: 80%;
margin-left: calc(var(--avatarSize) + var(--gutterSize));
margin-right: calc(var(--gutterSize) + var(--avatarSize));
.mx_EventListSummary_toggle {
float: none;
margin: 0;
order: 9;
margin-left: 5px;
margin-right: 55px;
}
.mx_EventListSummary_avatars {
padding-top: 0;
}
&::after {
content: "";
clear: both;
}
.mx_EventTile {
margin: 0 6px;
padding: 2px 0;
}
.mx_EventTile_line {
margin: 0 5px;
> a {
left: auto;
right: 0;
transform: translateX(calc(100% + 5px));
}
}
.mx_MessageActionBar {
transform: translate3d(90%, 0, 0);
}
}
.mx_EventListSummary[data-expanded=false][data-layout=bubble] {
padding: 0 34px;
}
/* events that do not require bubble layout */
.mx_EventListSummary[data-layout=bubble],
.mx_EventTile.mx_EventTile_bad[data-layout=bubble] {
.mx_EventTile_line {
background: transparent;
}
&:hover {
&::before {
background: transparent;
}
}
}

View File

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,102 +18,308 @@ limitations under the License.
$left-gutter: 64px;
$hover-select-border: 4px;
.mx_EventTile {
.mx_EventTile:not([data-layout=bubble]) {
max-width: 100%;
clear: both;
padding-top: 18px;
font-size: $font-14px;
position: relative;
}
.mx_EventTile.mx_EventTile_info {
padding-top: 1px;
}
&.mx_EventTile_info {
padding-top: 1px;
}
.mx_EventTile_avatar {
top: 14px;
left: 8px;
cursor: pointer;
user-select: none;
}
.mx_EventTile_avatar {
top: 14px;
left: 8px;
cursor: pointer;
user-select: none;
}
.mx_EventTile.mx_EventTile_info .mx_EventTile_avatar {
top: $font-6px;
left: $left-gutter;
}
&.mx_EventTile_info .mx_EventTile_avatar {
top: $font-6px;
left: $left-gutter;
}
.mx_EventTile_continuation {
padding-top: 0px !important;
&.mx_EventTile_continuation {
padding-top: 0px !important;
&.mx_EventTile_isEditing {
padding-top: 5px !important;
margin-top: -5px;
}
}
&.mx_EventTile_isEditing {
padding-top: 5px !important;
margin-top: -5px;
background-color: $header-panel-bg-color;
}
}
.mx_EventTile_isEditing {
background-color: $header-panel-bg-color;
}
.mx_SenderProfile {
color: $primary-fg-color;
font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden;
cursor: pointer;
padding-bottom: 0px;
padding-top: 0px;
margin: 0px;
/* the next three lines, along with overflow hidden, truncate long display names */
white-space: nowrap;
text-overflow: ellipsis;
max-width: calc(100% - $left-gutter);
}
.mx_EventTile .mx_SenderProfile {
color: $primary-fg-color;
font-size: $font-14px;
display: inline-block; /* anti-zalgo, with overflow hidden */
overflow: hidden;
cursor: pointer;
padding-bottom: 0px;
padding-top: 0px;
margin: 0px;
/* the next three lines, along with overflow hidden, truncate long display names */
white-space: nowrap;
text-overflow: ellipsis;
max-width: calc(100% - $left-gutter);
}
.mx_SenderProfile .mx_Flair {
opacity: 0.7;
margin-left: 5px;
display: inline-block;
vertical-align: top;
overflow: hidden;
user-select: none;
.mx_EventTile .mx_SenderProfile .mx_Flair {
opacity: 0.7;
margin-left: 5px;
display: inline-block;
vertical-align: top;
overflow: hidden;
user-select: none;
img {
vertical-align: -2px;
margin-right: 2px;
border-radius: 8px;
}
}
img {
vertical-align: -2px;
margin-right: 2px;
&.mx_EventTile_isEditing .mx_MessageTimestamp {
visibility: hidden;
}
.mx_MessageTimestamp {
display: block;
white-space: nowrap;
left: 0px;
text-align: center;
user-select: none;
}
&.mx_EventTile_continuation .mx_EventTile_line {
clear: both;
}
.mx_EventTile_line, .mx_EventTile_reply {
position: relative;
padding-left: $left-gutter;
border-radius: 8px;
}
}
.mx_EventTile_isEditing .mx_MessageTimestamp {
visibility: hidden;
}
.mx_EventTile .mx_MessageTimestamp {
display: block;
white-space: nowrap;
left: 0px;
text-align: center;
user-select: none;
}
.mx_EventTile_continuation .mx_EventTile_line {
clear: both;
}
.mx_EventTile_line, .mx_EventTile_reply {
position: relative;
padding-left: $left-gutter;
border-radius: 8px;
}
.mx_RoomView_timeline_rr_enabled,
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
.mx_EventListSummary {
.mx_EventTile_line {
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
margin-right: 110px;
.mx_EventTile_reply {
margin-right: 10px;
}
&.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
}
/* this is used for the tile for the event which is selected via the URL.
* TODO: ultimately we probably want some transition on here.
*/
&.mx_EventTile_selected > .mx_EventTile_line {
border-left: $accent-color 4px solid;
padding-left: calc($left-gutter - $hover-select-border);
background-color: $event-selected-color;
}
&.mx_EventTile_highlight,
&.mx_EventTile_highlight .markdown-body {
color: $event-highlight-fg-color;
.mx_EventTile_line {
background-color: $event-highlight-bg-color;
}
}
&.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
&.mx_EventTile:hover .mx_EventTile_line,
&.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
&.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
background-color: $event-selected-color;
}
.mx_EventTile_searchHighlight {
background-color: $accent-color;
color: $accent-fg-color;
border-radius: 5px;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
.mx_EventTile_searchHighlight a {
background-color: $accent-color;
color: $accent-fg-color;
}
.mx_EventTile_receiptSent,
.mx_EventTile_receiptSending {
// We don't use `position: relative` on the element because then it won't line
// up with the other read receipts
&::before {
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 14px;
width: 14px;
height: 14px;
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
}
}
.mx_EventTile_receiptSent::before {
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
}
.mx_EventTile_receiptSending::before {
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
}
&.mx_EventTile_contextual {
opacity: 0.4;
}
.mx_EventTile_msgOption {
float: right;
text-align: right;
position: relative;
width: 90px;
/* Hack to stop the height of this pushing the messages apart.
Replaces margin-top: -6px. This interacts better with a read
marker being in between. Content overflows. */
height: 1px;
margin-right: 10px;
}
.mx_EventTile_msgOption a {
text-decoration: none;
}
/* De-zalgoing */
.mx_EventTile_body {
overflow-y: hidden;
}
&:hover.mx_EventTile_verified .mx_EventTile_line,
&:hover.mx_EventTile_unverified .mx_EventTile_line,
&:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: calc($left-gutter - $hover-select-border);
}
&:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
}
&:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
}
&:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
}
&:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
&:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
&:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
/* End to end encryption stuff */
&:hover .mx_EventTile_e2eIcon {
opacity: 1;
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
&:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
&:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
&:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
&:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
&:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
&:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
display: block;
left: 41px;
}
.mx_MImageBody {
margin-right: 34px;
}
.mx_EventTile_e2eIcon {
position: absolute;
top: 6px;
left: 44px;
bottom: 0;
right: 0;
}
.mx_ReactionsRow {
margin: 0;
padding: 4px 64px;
}
}
.mx_EventTile:not([data-layout=bubble]).mx_EventTile_info .mx_EventTile_line,
.mx_EventListSummary:not([data-layout=bubble]) > :not(.mx_EventTile) .mx_EventTile_avatar ~ .mx_EventTile_line {
padding-left: calc($left-gutter + 18px);
}
.mx_EventListSummary:not([data-layout=bubble]) .mx_EventTile_line {
padding-left: calc($left-gutter);
}
/* all the overflow-y: hidden; are to trap Zalgos -
but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random
horizontal scrollbars occasionally appearing, like in
https://github.com/vector-im/vector-web/issues/1154 */
.mx_EventTile_content {
overflow-y: hidden;
overflow-x: hidden;
margin-right: 34px;
}
/* Spoiler stuff */
.mx_EventTile_spoiler {
cursor: pointer;
}
.mx_EventTile_spoiler_reason {
color: $event-timestamp-color;
font-size: $font-11px;
}
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
}
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
filter: none;
}
.mx_RoomView_timeline_rr_enabled {
.mx_EventTile:not([data-layout=bubble]) {
.mx_EventTile_line {
/* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */
margin-right: 110px;
}
}
// on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter
}
.mx_EventTile_bubbleContainer {
@ -130,123 +336,15 @@ $hover-select-border: 4px;
.mx_EventTile_msgOption {
grid-column: 2;
}
}
.mx_EventTile_reply {
margin-right: 10px;
}
/* HACK to override line-height which is already marked important elsewhere */
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
font-size: 48px !important;
line-height: 57px !important;
}
.mx_EventTile_selected > div > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
}
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
visibility: visible;
}
/* this is used for the tile for the event which is selected via the URL.
* TODO: ultimately we probably want some transition on here.
*/
.mx_EventTile_selected > .mx_EventTile_line {
border-left: $accent-color 4px solid;
padding-left: calc($left-gutter - $hover-select-border);
background-color: $event-selected-color;
}
.mx_EventTile_highlight,
.mx_EventTile_highlight .markdown-body {
color: $event-highlight-fg-color;
.mx_EventTile_line {
background-color: $event-highlight-bg-color;
&:hover {
.mx_EventTile_line {
// To avoid bubble events being highlighted
background-color: inherit !important;
}
}
}
.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px);
}
.mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
.mx_EventTile:hover .mx_EventTile_line,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_EventTile_line,
.mx_EventTile.focus-visible:focus-within .mx_EventTile_line {
background-color: $event-selected-color;
}
.mx_EventTile_searchHighlight {
background-color: $accent-color;
color: $accent-fg-color;
border-radius: 5px;
padding-left: 2px;
padding-right: 2px;
cursor: pointer;
}
.mx_EventTile_searchHighlight a {
background-color: $accent-color;
color: $accent-fg-color;
}
.mx_EventTile_receiptSent,
.mx_EventTile_receiptSending {
// We don't use `position: relative` on the element because then it won't line
// up with the other read receipts
&::before {
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 14px;
width: 14px;
height: 14px;
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
}
}
.mx_EventTile_receiptSent::before {
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
}
.mx_EventTile_receiptSending::before {
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
}
.mx_EventTile_contextual {
opacity: 0.4;
}
.mx_EventTile_msgOption {
float: right;
text-align: right;
position: relative;
width: 90px;
/* Hack to stop the height of this pushing the messages apart.
Replaces margin-top: -6px. This interacts better with a read
marker being in between. Content overflows. */
height: 1px;
margin-right: 10px;
}
.mx_EventTile_msgOption a {
text-decoration: none;
}
.mx_EventTile_readAvatars {
position: relative;
display: inline-block;
@ -277,52 +375,27 @@ $hover-select-border: 4px;
position: absolute;
}
/* all the overflow-y: hidden; are to trap Zalgos -
but they introduce an implicit overflow-x: auto.
so make that explicitly hidden too to avoid random
horizontal scrollbars occasionally appearing, like in
https://github.com/vector-im/vector-web/issues/1154
*/
.mx_EventTile_content {
display: block;
overflow-y: hidden;
overflow-x: hidden;
margin-right: 34px;
/* HACK to override line-height which is already marked important elsewhere */
.mx_EventTile_bigEmoji.mx_EventTile_bigEmoji {
font-size: 48px !important;
line-height: 57px !important;
}
/* De-zalgoing */
.mx_EventTile_body {
overflow-y: hidden;
}
/* Spoiler stuff */
.mx_EventTile_spoiler {
.mx_EventTile_content .mx_EventTile_edited {
user-select: none;
font-size: $font-12px;
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
cursor: pointer;
}
.mx_EventTile_spoiler_reason {
color: $event-timestamp-color;
font-size: $font-11px;
}
.mx_EventTile_spoiler_content {
filter: blur(5px) saturate(0.1) sepia(1);
transition-duration: 0.5s;
}
.mx_EventTile_spoiler.visible > .mx_EventTile_spoiler_content {
filter: none;
}
.mx_EventTile_e2eIcon {
position: absolute;
top: 6px;
left: 44px;
position: relative;
width: 14px;
height: 14px;
display: block;
bottom: 0;
right: 0;
opacity: 0.2;
background-repeat: no-repeat;
background-size: contain;
@ -381,87 +454,6 @@ $hover-select-border: 4px;
opacity: 1;
}
.mx_EventTile_keyRequestInfo {
font-size: $font-12px;
}
.mx_EventTile_keyRequestInfo_text {
opacity: 0.5;
}
.mx_EventTile_keyRequestInfo_text a {
color: $primary-fg-color;
text-decoration: underline;
cursor: pointer;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p {
text-align: auto;
margin-left: 3px;
margin-right: 3px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
margin-top: 0px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
margin-bottom: 0px;
}
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
padding-left: calc($left-gutter - $hover-select-border);
}
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line {
border-left: $e2e-verified-color $EventTile_e2e_state_indicator_width solid;
}
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line {
border-left: $e2e-unverified-color $EventTile_e2e_state_indicator_width solid;
}
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line {
border-left: $e2e-unknown-color $EventTile_e2e_state_indicator_width solid;
}
.mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line,
.mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line {
padding-left: calc($left-gutter + 18px - $hover-select-border);
}
/* End to end encryption stuff */
.mx_EventTile:hover .mx_EventTile_e2eIcon {
opacity: 1;
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp {
left: calc(-$hover-select-border);
}
// Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies)
.mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > .mx_EventTile_e2eIcon,
.mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > .mx_EventTile_e2eIcon,
.mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > .mx_EventTile_e2eIcon {
display: block;
left: 41px;
}
.mx_EventTile_content .mx_EventTile_edited {
user-select: none;
font-size: $font-12px;
color: $roomtopic-color;
display: inline-block;
margin-left: 9px;
cursor: pointer;
}
/* Various markdown overrides */
.mx_EventTile_body pre {
@ -480,6 +472,10 @@ $hover-select-border: 4px;
background-color: $header-panel-bg-color;
}
pre code > * {
display: inline-block;
}
pre {
// have to use overlay rather than auto otherwise Linux and Windows
// Chrome gets very confused about vertical spacing:
@ -577,6 +573,12 @@ $hover-select-border: 4px;
color: $accent-color-alt;
}
.mx_EventTile_content .markdown-body blockquote {
border-left: 2px solid $blockquote-bar-color;
border-radius: 2px;
padding: 0 10px;
}
.mx_EventTile_content .markdown-body .hljs {
display: inline !important;
}
@ -595,6 +597,35 @@ $hover-select-border: 4px;
/* end of overrides */
.mx_EventTile_keyRequestInfo {
font-size: $font-12px;
}
.mx_EventTile_keyRequestInfo_text {
opacity: 0.5;
}
.mx_EventTile_keyRequestInfo_text a {
color: $primary-fg-color;
text-decoration: underline;
cursor: pointer;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p {
text-align: auto;
margin-left: 3px;
margin-right: 3px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:first-child {
margin-top: 0px;
}
.mx_EventTile_keyRequestInfo_tooltip_contents p:last-child {
margin-bottom: 0px;
}
.mx_EventTile_tileError {
color: red;
text-align: center;
@ -615,6 +646,13 @@ $hover-select-border: 4px;
}
}
.mx_EventTile:hover .mx_MessageActionBar,
.mx_EventTile.mx_EventTile_actionBarFocused .mx_MessageActionBar,
[data-whatinput='keyboard'] .mx_EventTile:focus-within .mx_MessageActionBar,
.mx_EventTile.focus-visible:focus-within .mx_MessageActionBar {
visibility: visible;
}
@media only screen and (max-width: 480px) {
.mx_EventTile_line, .mx_EventTile_reply {
padding-left: 0;

View File

@ -26,6 +26,7 @@ $left-gutter: 64px;
> .mx_EventTile_avatar {
position: absolute;
z-index: 9;
}
.mx_MessageTimestamp {

View File

@ -116,6 +116,11 @@ $irc-line-height: $font-18px;
.mx_EditMessageComposer_buttons {
position: relative;
}
.mx_ReactionsRow {
padding-left: 0;
padding-right: 0;
}
}
.mx_EventTile_emote {

View File

@ -19,7 +19,8 @@ limitations under the License.
margin-right: 15px;
margin-bottom: 15px;
display: flex;
border-left: 4px solid $preview-widget-bar-color;
border-left: 2px solid $preview-widget-bar-color;
border-radius: 2px;
color: $preview-widget-fg-color;
}
@ -33,7 +34,7 @@ limitations under the License.
.mx_LinkPreviewWidget_caption {
margin-left: 15px;
flex: 1 1 auto;
overflow-x: hidden; // cause it to wrap rather than clip
overflow: hidden; // cause it to wrap rather than clip
}
.mx_LinkPreviewWidget_title {

View File

@ -165,8 +165,6 @@ limitations under the License.
font-size: $font-14px;
max-height: 120px;
overflow: auto;
/* needed for FF */
font-family: $font-family;
}
/* hack for FF as vertical alignment of custom placeholder text is broken */

View File

@ -60,8 +60,6 @@ limitations under the License.
$reply-lines: 2;
$line-height: $font-22px;
pointer-events: none;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;

View File

@ -29,8 +29,10 @@ limitations under the License.
display: flex;
flex-direction: column;
// min-height at this level so the mx_BasicMessageComposer_input
// still stays vertically centered when less than 50px
min-height: 50px;
// still stays vertically centered when less than 55px.
// We also set this to ensure the voice message recording widget
// doesn't cause a jump.
min-height: 55px;
.mx_BasicMessageComposer_input {
padding: 3px 0;

View File

@ -36,7 +36,6 @@ limitations under the License.
.mx_SettingsTab_subheading {
font-size: $font-16px;
display: block;
font-family: $font-family;
font-weight: 600;
color: $primary-fg-color;
margin-bottom: 10px;
@ -47,14 +46,14 @@ limitations under the License.
color: $settings-subsection-fg-color;
font-size: $font-14px;
display: block;
margin: 10px 100px 10px 0; // Align with the rest of the view
margin: 10px 80px 10px 0; // Align with the rest of the view
}
.mx_SettingsTab_section {
margin-bottom: 24px;
.mx_SettingsFlag {
margin-right: 100px;
margin-right: 80px;
margin-bottom: 10px;
}

View File

@ -14,6 +14,44 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_SecurityRoomSettingsTab {
.mx_SettingsTab_showAdvanced {
padding: 0;
margin-bottom: 16px;
}
.mx_SecurityRoomSettingsTab_spacesWithAccess {
> h4 {
color: $secondary-fg-color;
font-weight: $font-semi-bold;
font-size: $font-12px;
line-height: $font-15px;
text-transform: uppercase;
}
> span {
font-weight: 500;
font-size: $font-14px;
line-height: 32px; // matches height of avatar for v-align
color: $secondary-fg-color;
display: inline-block;
img.mx_RoomAvatar_isSpaceRoom,
.mx_RoomAvatar_isSpaceRoom img {
border-radius: 8px;
}
.mx_BaseAvatar {
margin-right: 8px;
}
& + span {
margin-left: 16px;
}
}
}
}
.mx_SecurityRoomSettingsTab_warning {
display: block;
@ -26,5 +64,51 @@ limitations under the License.
}
.mx_SecurityRoomSettingsTab_encryptionSection {
margin-bottom: 25px;
padding-bottom: 24px;
border-bottom: 1px solid $menu-border-color;
margin-bottom: 32px;
}
.mx_SecurityRoomSettingsTab_upgradeRequired {
margin-left: 16px;
padding: 4px 16px;
border: 1px solid $accent-color;
border-radius: 8px;
color: $accent-color;
font-size: $font-12px;
line-height: $font-15px;
}
.mx_SecurityRoomSettingsTab_joinRule {
.mx_RadioButton {
padding-top: 16px;
margin-bottom: 8px;
.mx_RadioButton_content {
margin-left: 14px;
font-weight: $font-semi-bold;
font-size: $font-15px;
line-height: $font-24px;
color: $primary-fg-color;
display: block;
}
}
> span {
display: inline-block;
margin-left: 34px;
margin-bottom: 16px;
font-size: $font-15px;
line-height: $font-24px;
color: $secondary-fg-color;
& + .mx_RadioButton {
border-top: 1px solid $menu-border-color;
}
}
.mx_AccessibleButton_kind_link {
padding: 0;
font-size: inherit;
}
}

View File

@ -15,8 +15,7 @@ limitations under the License.
*/
.mx_AppearanceUserSettingsTab_fontSlider,
.mx_AppearanceUserSettingsTab_fontSlider_preview,
.mx_AppearanceUserSettingsTab_Layout {
.mx_AppearanceUserSettingsTab_fontSlider_preview {
@mixin mx_Settings_fullWidthField;
}
@ -45,6 +44,11 @@ limitations under the License.
border-radius: 10px;
padding: 0 16px 9px 16px;
pointer-events: none;
display: flow-root;
.mx_EventTile[data-layout=bubble] {
margin-top: 30px;
}
.mx_EventTile_msgOption {
display: none;
@ -154,13 +158,10 @@ limitations under the License.
.mx_AppearanceUserSettingsTab_Layout_RadioButtons {
display: flex;
flex-direction: row;
gap: 24px;
color: $primary-fg-color;
.mx_AppearanceUserSettingsTab_spacer {
width: 24px;
}
> .mx_AppearanceUserSettingsTab_Layout_RadioButton {
flex-grow: 0;
flex-shrink: 1;
@ -210,6 +211,21 @@ limitations under the License.
.mx_RadioButton_checked {
background-color: rgba($accent-color, 0.08);
}
.mx_EventTile {
margin: 0;
&[data-layout=bubble] {
margin-right: 40px;
}
&[data-layout=irc] {
> a {
display: none;
}
}
.mx_EventTile_line {
max-width: 90%;
}
}
}
.mx_AppearanceUserSettingsTab_Advanced {

View File

@ -67,7 +67,26 @@ limitations under the License.
.mx_CallView_content {
position: relative;
display: flex;
justify-content: center;
border-radius: 8px;
> .mx_VideoFeed {
width: 100%;
height: 100%;
&.mx_VideoFeed_voice {
// We don't want to collide with the call controls that have 52px of height
padding-bottom: 52px;
background-color: $inverted-bg-color;
display: flex;
justify-content: center;
align-items: center;
}
&.mx_VideoFeed_video {
background-color: #000;
}
}
}
.mx_CallView_voice {
@ -260,7 +279,7 @@ limitations under the License.
max-width: 240px;
}
.mx_CallView_header_phoneIcon {
.mx_CallView_header_callTypeIcon {
display: inline-block;
margin-right: 6px;
height: 16px;
@ -274,12 +293,19 @@ limitations under the License.
height: 16px;
width: 16px;
background-color: $warning-color;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
&.mx_CallView_header_callTypeIcon_voice::before {
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
&.mx_CallView_header_callTypeIcon_video::before {
mask-image: url('$(res)/img/element-icons/call/video-call.svg');
}
}
.mx_CallView_callControls {
@ -287,9 +313,9 @@ limitations under the License.
display: flex;
justify-content: center;
bottom: 5px;
width: 100%;
opacity: 1;
transition: opacity 0.5s;
z-index: 200; // To be above _all_ feeds
}
.mx_CallView_callControls_hidden {
@ -297,10 +323,29 @@ limitations under the License.
pointer-events: none;
}
.mx_CallView_presenting {
opacity: 1;
transition: opacity 0.5s;
position: absolute;
margin-top: 18px;
padding: 4px 8px;
border-radius: 4px;
// Same on both themes
color: white;
background-color: #17191c;
}
.mx_CallView_presenting_hidden {
opacity: 0.001; // opacity 0 can cause a re-layout
pointer-events: none;
}
.mx_CallView_callControls_button {
cursor: pointer;
margin-left: 8px;
margin-right: 8px;
margin-left: 2px;
margin-right: 2px;
&::before {
@ -317,17 +362,11 @@ limitations under the License.
}
.mx_CallView_callControls_dialpad {
margin-right: auto;
&::before {
background-image: url('$(res)/img/voip/dialpad.svg');
}
}
.mx_CallView_callControls_button_dialpad_hidden {
margin-right: auto;
cursor: initial;
}
.mx_CallView_callControls_button_micOn {
&::before {
background-image: url('$(res)/img/voip/mic-on.svg');
@ -352,6 +391,30 @@ limitations under the License.
}
}
.mx_CallView_callControls_button_screensharingOn {
&::before {
background-image: url('$(res)/img/voip/screensharing-on.svg');
}
}
.mx_CallView_callControls_button_screensharingOff {
&::before {
background-image: url('$(res)/img/voip/screensharing-off.svg');
}
}
.mx_CallView_callControls_button_sidebarOn {
&::before {
background-image: url('$(res)/img/voip/sidebar-on.svg');
}
}
.mx_CallView_callControls_button_sidebarOff {
&::before {
background-image: url('$(res)/img/voip/sidebar-off.svg');
}
}
.mx_CallView_callControls_button_hangup {
&::before {
background-image: url('$(res)/img/voip/hangup.svg');
@ -359,17 +422,11 @@ limitations under the License.
}
.mx_CallView_callControls_button_more {
margin-left: auto;
&::before {
background-image: url('$(res)/img/voip/more.svg');
}
}
.mx_CallView_callControls_button_more_hidden {
margin-left: auto;
cursor: initial;
}
.mx_CallView_callControls_button_invisible {
visibility: hidden;
pointer-events: none;

View File

@ -0,0 +1,52 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CallViewSidebar {
position: absolute;
right: 16px;
bottom: 16px;
z-index: 100; // To be above the primary feed
overflow: auto;
height: calc(100% - 32px); // Subtract the top and bottom padding
width: 20%;
display: flex;
flex-direction: column-reverse;
justify-content: flex-start;
align-items: flex-end;
gap: 12px;
> .mx_VideoFeed {
width: 100%;
&.mx_VideoFeed_voice {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 16 / 9;
}
}
&.mx_CallViewSidebar_pipMode {
top: 16px;
bottom: unset;
justify-content: flex-end;
gap: 4px;
}
}

View File

@ -14,32 +14,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_VideoFeed_voice {
background-color: $inverted-bg-color;
}
.mx_VideoFeed_remote {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
&.mx_VideoFeed_video {
background-color: #000;
}
}
.mx_VideoFeed_local {
max-width: 25%;
max-height: 25%;
position: absolute;
right: 10px;
top: 10px;
z-index: 100;
.mx_VideoFeed {
border-radius: 4px;
&.mx_VideoFeed_voice {
background-color: $inverted-bg-color;
}
&.mx_VideoFeed_video {
background-color: transparent;
}

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16ZM6.9806 4.5101C6.9306 3.9401 7.3506 3.4401 7.9206 3.4001C8.4806 3.3601 8.9806 3.7801 9.0406 4.3501V4.5101L8.7206 8.5101C8.6906 8.8801 8.3806 9.1601 8.0106 9.1601H7.9506C7.6006 9.1301 7.3306 8.8601 7.3006 8.5101L6.9806 4.5101ZM8.88012 11.1202C8.88012 11.6062 8.48613 12.0002 8.00012 12.0002C7.51411 12.0002 7.12012 11.6062 7.12012 11.1202C7.12012 10.6342 7.51411 10.2402 8.00012 10.2402C8.48613 10.2402 8.88012 10.6342 8.88012 11.1202Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View File

@ -1,7 +0,0 @@
<svg height="12" viewBox="0 0 12 12" width="12" xmlns="http://www.w3.org/2000/svg">
<g style="stroke:#454545;stroke-width:.8;fill:none;fill-rule:evenodd;stroke-linecap:round;stroke-linejoin:round" transform="translate(1 1)">
<circle cx="5" cy="5" r="5"/>
<path d="m0 5h10"/>
<path d="m5 0c1.25064019 1.36917645 1.96137638 3.14601693 2 5-.03862362 1.85398307-.74935981 3.63082355-2 5-1.25064019-1.36917645-1.96137638-3.14601693-2-5 .03862362-1.85398307.74935981-3.63082355 2-5z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 524 B

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 4.81815C0 3.76379 0.89543 2.90906 2 2.90906H9.33333C10.4379 2.90906 11.3333 3.76379 11.3333 4.81815V11.1818C11.3333 12.2361 10.4379 13.0909 9.33333 13.0909H2C0.895429 13.0909 0 12.2361 0 11.1818V4.81815ZM12.6667 6.09089L14.9169 4.37255C15.3534 4.03921 16 4.33587 16 4.86947V11.1305C16 11.6641 15.3534 11.9607 14.9169 11.6274L12.6667 9.90907V6.09089ZM3.68584 8.54792C3.68584 8.82819 3.45653 9.05751 3.17625 9.05751C2.89598 9.05751 2.66667 8.82819 2.66667 8.54792V6.50957C2.66667 6.22929 2.89598 5.99998 3.17625 5.99998H5.2146C5.49488 5.99998 5.72419 6.22929 5.72419 6.50957C5.72419 6.78984 5.49488 7.01916 5.2146 7.01916H4.39926L6.2083 8.82819L8.73076 6.30573C8.9295 6.10699 9.25054 6.10699 9.44928 6.30573C9.64802 6.50447 9.64802 6.82551 9.44928 7.02425L6.56501 9.90852C6.36627 10.1073 6.04523 10.1073 5.84649 9.90852L3.68584 7.74787V8.54792Z" fill="#8D97A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1016 B

View File

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.00016 6C4.36683 6 4.66683 5.7 4.66683 5.33333V4.28667L7.4935 7.11333C7.7535 7.37333 8.1735 7.37333 8.4335 7.11333L12.2068 3.34C12.4668 3.08 12.4668 2.66 12.2068 2.4C11.9468 2.14 11.5268 2.14 11.2668 2.4L7.96683 5.7L5.60016 3.33333H6.66683C7.0335 3.33333 7.3335 3.03333 7.3335 2.66667C7.3335 2.3 7.0335 2 6.66683 2H4.00016C3.6335 2 3.3335 2.3 3.3335 2.66667V5.33333C3.3335 5.7 3.6335 6 4.00016 6Z" fill="#8D97A5"/>
<path d="M8.00557 8.67107C6.88076 8.62784 4.56757 8.91974 4.0052 9.06763C3.97195 9.07638 3.93363 9.08616 3.89078 9.0971C3.02734 9.31746 0.321813 10.008 0.0294949 12.1958C-0.196977 13.8909 0.937169 14.4039 1.50412 14.3258C1.89653 14.2766 3.02006 14.0989 4.05816 13.9127C5.07753 13.7298 5.07701 13.0573 5.07666 12.6026C5.07665 12.5943 5.07664 12.586 5.07664 12.5778L5.07665 11.6636C5.07665 11.4308 5.29543 11.2962 5.5972 11.2598C6.66548 11.1147 7.5573 11.1143 8.00369 11.1143L8.00745 11.1143C8.45377 11.1143 9.33453 11.1147 10.4028 11.2598C10.7046 11.2962 10.9234 11.4308 10.9234 11.6636L10.9234 12.5778C10.9234 12.586 10.9233 12.5943 10.9233 12.6026C10.923 13.0573 10.9225 13.7298 11.9418 13.9127C12.9799 14.099 14.1035 14.2766 14.4959 14.3258C15.0628 14.4039 16.197 13.8909 15.9705 12.1958C15.6782 10.008 12.9727 9.31747 12.1092 9.0971C12.0664 9.08617 12.0281 9.07639 11.9948 9.06764C11.4324 8.91975 9.13037 8.62783 8.00557 8.67107Z" fill="#8D97A5"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<svg width="50" height="49" viewBox="0 0 50 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="25" cy="20" r="20" fill="white"/>
</g>
<rect x="14.6008" y="12.8" width="20.8" height="14.4" rx="1.6" fill="white" stroke="#737D8C" stroke-width="1.6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.3617 23.36C24.3617 23.7135 24.6483 24 25.0017 24C25.3552 24 25.6417 23.7135 25.6417 23.36L25.6417 18.1851L27.6692 20.2125C27.9191 20.4625 28.3243 20.4625 28.5743 20.2125C28.8242 19.9626 28.8242 19.5574 28.5743 19.3075L25.4543 16.1875C25.2043 15.9375 24.7991 15.9375 24.5492 16.1875L21.4292 19.3075C21.1792 19.5574 21.1792 19.9626 21.4292 20.2125C21.6791 20.4625 22.0843 20.4625 22.3343 20.2125L24.3617 18.1851L24.3617 23.36Z" fill="#737D8C"/>
<defs>
<filter id="filter0_d" x="0.947663" y="0" width="48.1047" height="48.1047" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4.05234"/>
<feGaussianBlur stdDeviation="2.02617"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<svg width="50" height="49" viewBox="0 0 50 49" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="25" cy="20" r="20" fill="#0DBD8B"/>
</g>
<rect x="14.6008" y="12.8" width="20.8" height="14.4" rx="1.6" fill="#0DBD8B" stroke="white" stroke-width="1.6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.3617 23.36C24.3617 23.7135 24.6483 24 25.0017 24C25.3552 24 25.6417 23.7135 25.6417 23.36L25.6417 18.1851L27.6692 20.2125C27.9191 20.4625 28.3243 20.4625 28.5743 20.2125C28.8242 19.9626 28.8242 19.5574 28.5743 19.3075L25.4543 16.1875C25.2043 15.9375 24.7991 15.9375 24.5492 16.1875L21.4292 19.3075C21.1792 19.5574 21.1792 19.9626 21.4292 20.2125C21.6791 20.4625 22.0843 20.4625 22.3343 20.2125L24.3617 18.1851L24.3617 23.36Z" fill="white"/>
<defs>
<filter id="filter0_d" x="0.947663" y="0" width="48.1047" height="48.1047" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4.05234"/>
<feGaussianBlur stdDeviation="2.02617"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,20 @@
<svg width="48" height="47" viewBox="0 0 48 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="#737D8C"/>
</g>
<rect x="12.5618" y="12.8992" width="20.3525" height="14.4496" rx="2.43819" fill="white" stroke="#737D8C" stroke-width="1.12362"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.9132 20.5009C33.2675 20.5009 34.3655 19.4205 34.3655 18.0876C34.3655 16.7548 33.2675 15.6743 31.9132 15.6743C30.5589 15.6743 29.4609 16.7548 29.4609 18.0876C29.4609 19.4205 30.5589 20.5009 31.9132 20.5009ZM27.8242 26.132C27.8282 23.7187 28.976 21.3054 31.9113 21.3054C34.7818 21.3054 35.9984 23.7187 35.9984 26.132C35.9984 28.5453 32.7288 28.5453 31.9113 28.5453C31.0939 28.5453 27.8206 28.3403 27.8242 26.132Z" fill="white"/>
<path d="M27.8242 26.132L28.386 26.1329L27.8242 26.132ZM35.9984 26.132H35.4366H35.9984ZM33.8037 18.0876C33.8037 19.1017 32.9658 19.9391 31.9132 19.9391V21.0627C33.5693 21.0627 34.9273 19.7392 34.9273 18.0876H33.8037ZM31.9132 16.2361C32.9658 16.2361 33.8037 17.0735 33.8037 18.0876H34.9273C34.9273 16.4361 33.5693 15.1125 31.9132 15.1125V16.2361ZM30.0227 18.0876C30.0227 17.0735 30.8606 16.2361 31.9132 16.2361V15.1125C30.2571 15.1125 28.8991 16.4361 28.8991 18.0876H30.0227ZM31.9132 19.9391C30.8606 19.9391 30.0227 19.1017 30.0227 18.0876H28.8991C28.8991 19.7392 30.2571 21.0627 31.9132 21.0627V19.9391ZM31.9113 20.7436C30.2659 20.7436 29.0747 21.4314 28.3132 22.4845C27.5693 23.5133 27.2645 24.8471 27.2624 26.1311L28.386 26.1329C28.3879 25.0036 28.659 23.924 29.2238 23.1429C29.771 22.386 30.6214 21.8672 31.9113 21.8672V20.7436ZM36.5602 26.132C36.5602 24.8414 36.2364 23.5081 35.4845 22.4817C34.7168 21.4338 33.5275 20.7436 31.9113 20.7436V21.8672C33.1657 21.8672 34.0199 22.3836 34.5781 23.1457C35.1521 23.9293 35.4366 25.0093 35.4366 26.132H36.5602ZM31.9113 29.1071C32.3157 29.1071 33.4213 29.1105 34.4365 28.7775C34.9481 28.6096 35.4778 28.3438 35.8839 27.9122C36.3025 27.4673 36.5602 26.8767 36.5602 26.132H35.4366C35.4366 26.594 35.2857 26.9083 35.0656 27.1422C34.8331 27.3893 34.4943 27.576 34.0863 27.7098C33.2623 27.9801 32.3244 27.9835 31.9113 27.9835V29.1071ZM27.2624 26.1311C27.26 27.5996 28.3757 28.3418 29.3716 28.6961C30.3797 29.0547 31.4763 29.1071 31.9113 29.1071V27.9835C31.5289 27.9835 30.5802 27.9334 29.7482 27.6375C28.9039 27.3371 28.3848 26.8728 28.386 26.1329L27.2624 26.1311Z" fill="#737D8C"/>
<rect x="0.0339116" y="-0.787426" width="29.1443" height="3.36793" rx="1.68396" transform="matrix(0.681883 0.731461 -0.742244 0.670129 13.0943 8.71545)" fill="white" stroke="#737D8C" stroke-width="1.12362"/>
<defs>
<filter id="filter0_d" x="0.589744" y="0" width="46.8205" height="46.8205" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="3.41026"/>
<feGaussianBlur stdDeviation="1.70513"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,19 @@
<svg width="48" height="47" viewBox="0 0 48 47" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="white"/>
</g>
<rect x="12.5" y="12.5" width="20.4763" height="15.3319" rx="2.5" fill="#737D8C" stroke="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M31.912 20.5618C33.2664 20.5618 34.3643 19.4287 34.3643 18.0309C34.3643 16.6331 33.2664 15.5 31.912 15.5C30.5577 15.5 29.4598 16.6331 29.4598 18.0309C29.4598 19.4287 30.5577 20.5618 31.912 20.5618ZM27.8242 26.467C27.8282 23.9361 28.976 21.4052 31.9113 21.4052C34.7818 21.4052 35.9985 23.9361 35.9985 26.467C35.9985 28.9978 32.7288 28.9978 31.9114 28.9978C31.0939 28.9978 27.8206 28.7829 27.8242 26.467Z" fill="#737D8C"/>
<path d="M27.8242 26.467L27.3242 26.4662L27.8242 26.467ZM35.9985 26.467H36.4985H35.9985ZM33.8643 18.0309C33.8643 19.1675 32.9755 20.0618 31.912 20.0618V21.0618C33.5573 21.0618 34.8643 19.6898 34.8643 18.0309H33.8643ZM31.912 16C32.9755 16 33.8643 16.8943 33.8643 18.0309H34.8643C34.8643 16.372 33.5573 15 31.912 15V16ZM29.9598 18.0309C29.9598 16.8943 30.8486 16 31.912 16V15C30.2668 15 28.9598 16.372 28.9598 18.0309H29.9598ZM31.912 20.0618C30.8486 20.0618 29.9598 19.1675 29.9598 18.0309H28.9598C28.9598 19.6898 30.2668 21.0618 31.912 21.0618V20.0618ZM31.9113 20.9052C30.2753 20.9052 29.1023 21.622 28.3569 22.7032C27.6274 23.7612 27.3263 25.1361 27.3242 26.4662L28.3242 26.4677C28.3261 25.2669 28.6009 24.1109 29.1802 23.2708C29.7434 22.4538 30.612 21.9052 31.9113 21.9052V20.9052ZM36.4985 26.467C36.4985 25.1313 36.1789 23.7567 35.4412 22.7007C34.6893 21.6242 33.5177 20.9052 31.9113 20.9052V21.9052C33.1755 21.9052 34.0475 22.4516 34.6214 23.2733C35.2097 24.1154 35.4985 25.2717 35.4985 26.467H36.4985ZM31.9114 29.4978C32.3162 29.4978 33.416 29.5011 34.4241 29.1543C34.9326 28.9794 35.4519 28.7044 35.847 28.264C36.2515 27.8131 36.4985 27.2184 36.4985 26.467H35.4985C35.4985 26.9809 35.3367 27.3354 35.1026 27.5962C34.8591 27.8677 34.5099 28.0673 34.0988 28.2087C33.2677 28.4946 32.3239 28.4978 31.9114 28.4978V29.4978ZM27.3242 26.4662C27.3219 27.9345 28.3854 28.6964 29.3851 29.0693C30.3864 29.4429 31.4779 29.4978 31.9114 29.4978V28.4978C31.5274 28.4978 30.5735 28.4453 29.7346 28.1324C28.8943 27.8189 28.3229 27.3153 28.3242 26.4677L27.3242 26.4662Z" fill="white"/>
<defs>
<filter id="filter0_d" x="0.589744" y="0" width="46.8205" height="46.8205" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="3.41026"/>
<feGaussianBlur stdDeviation="1.70513"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -209,8 +209,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049; // "Dark Tile"
$message-body-panel-icon-fg-color: #21262C; // "Separator"
$message-body-panel-icon-bg-color: $tertiary-fg-color;
$message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: #21262C; // "System Dark"
$voice-record-stop-border-color: $quaternary-fg-color;
$voice-record-waveform-incomplete-fg-color: $quaternary-fg-color;
@ -227,6 +227,13 @@ $groupFilterPanel-background-blur-amount: 30px;
$composer-shadow-color: rgba(0, 0, 0, 0.28);
// Bubble tiles
$eventbubble-self-bg: #143A34;
$eventbubble-others-bg: #394049;
$eventbubble-bg-hover: #433C23;
$eventbubble-avatar-outline: $bg-color;
$eventbubble-reply-color: #C1C6CD;
// ***** Mixins! *****
@define-mixin mx_DialogButton {
@ -288,3 +295,11 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28);
.hljs-tag {
color: inherit; // Without this they'd be weirdly blue which doesn't match the theme
}
.hljs-addition {
background: #1a4b59;
}
.hljs-deletion {
background: #53232a;
}

View File

@ -207,8 +207,8 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #394049;
$message-body-panel-icon-fg-color: $primary-bg-color;
$message-body-panel-icon-bg-color: $secondary-fg-color;
$message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: #21262C;
// See non-legacy dark for variable information
$voice-record-stop-border-color: #6F7882;

View File

@ -331,7 +331,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0;
$message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $primary-bg-color;
$message-body-panel-icon-bg-color: #F4F6FA;
// See non-legacy _light for variable information
$voice-record-stop-symbol-color: #ff4b55;
@ -347,6 +347,13 @@ $appearance-tab-border-color: $input-darker-bg-color;
$composer-shadow-color: tranparent;
// Bubble tiles
$eventbubble-self-bg: #F8FDFC;
$eventbubble-others-bg: #F7F8F9;
$eventbubble-bg-hover: rgb(242, 242, 242);
$eventbubble-avatar-outline: #fff;
$eventbubble-reply-color: #C1C6CD;
// ***** Mixins! *****
@define-mixin mx_DialogButton {

View File

@ -327,7 +327,7 @@ $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-fg-color: $secondary-fg-color;
$message-body-panel-bg-color: #E3E8F0; // "Separator"
$message-body-panel-icon-fg-color: $secondary-fg-color;
$message-body-panel-icon-bg-color: $primary-bg-color;
$message-body-panel-icon-bg-color: #F4F6FA;
// These two don't change between themes. They are the $warning-color, but we don't
// want custom themes to affect them by accident.
@ -349,6 +349,13 @@ $groupFilterPanel-background-blur-amount: 20px;
$composer-shadow-color: rgba(0, 0, 0, 0.04);
// Bubble tiles
$eventbubble-self-bg: #F8FDFC;
$eventbubble-others-bg: #F7F8F9;
$eventbubble-bg-hover: #FEFCF5;
$eventbubble-avatar-outline: $primary-bg-color;
$eventbubble-reply-color: #C1C6CD;
// ***** Mixins! *****
@define-mixin mx_DialogButton {

View File

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { JSXElementConstructor } from "react";
import React, { JSXElementConstructor } from "react";
// Based on https://stackoverflow.com/a/53229857/3532235
export type Without<T, U> = {[P in Exclude<keyof T, keyof U>]?: never};
@ -22,3 +22,4 @@ export type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<
export type Writeable<T> = { -readonly [P in keyof T]: T[P] };
export type ComponentClass = keyof JSX.IntrinsicElements | JSXElementConstructor<any>;
export type ReactAnyComponent = React.Component | React.ExoticComponent;

View File

@ -50,6 +50,8 @@ import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
/* eslint-disable @typescript-eslint/naming-convention */
declare global {
interface Window {
matrixChat: ReturnType<Renderer>;
@ -90,6 +92,7 @@ declare global {
mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore;
mxOnRecaptchaLoaded?: () => void;
}
interface Document {
@ -114,7 +117,7 @@ declare global {
}
interface StorageEstimate {
usageDetails?: {[key: string]: number};
usageDetails?: { [key: string]: number };
}
interface HTMLAudioElement {
@ -185,4 +188,21 @@ declare global {
parameterDescriptors?: AudioParamDescriptor[];
}
);
// eslint-disable-next-line no-var
var grecaptcha:
| undefined
| {
reset: (id: string) => void;
render: (
divId: string,
options: {
sitekey: string;
callback: (response: string) => void;
},
) => string;
isReady: () => boolean;
};
}
/* eslint-enable @typescript-eslint/naming-convention */

20
src/@types/svg.d.ts vendored Normal file
View File

@ -0,0 +1,20 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
declare module "*.svg" {
const path: string;
export default path;
}

View File

@ -270,7 +270,7 @@ export class Analytics {
localStorage.removeItem(LAST_VISIT_TS_KEY);
}
private async _track(data: IData) {
private async track(data: IData) {
if (this.disabled) return;
const now = new Date();
@ -304,7 +304,7 @@ export class Analytics {
}
public ping() {
this._track({
this.track({
ping: "1",
});
localStorage.setItem(LAST_VISIT_TS_KEY, String(new Date().getTime())); // update last visit ts
@ -324,14 +324,14 @@ export class Analytics {
// But continue anyway because we still want to track the change
}
this._track({
this.track({
gt_ms: String(generationTimeMs),
});
}
public trackEvent(category: string, action: string, name?: string, value?: string) {
if (this.disabled) return;
this._track({
this.track({
e_c: category,
e_a: action,
e_n: name,
@ -395,17 +395,17 @@ export class Analytics {
Modal.createTrackedDialog('Analytics Details', '', ErrorDialog, {
title: _t('Analytics'),
description: <div className="mx_AnalyticsModal">
<div>{_t('The information being sent to us to help make %(brand)s better includes:', {
<div>{ _t('The information being sent to us to help make %(brand)s better includes:', {
brand: SdkConfig.get().brand,
})}</div>
}) }</div>
<table>
{ rows.map((row) => <tr key={row[0]}>
<td>{_t(
<td>{ _t(
customVariables[row[0]].expl,
customVariables[row[0]].getTextVariables ?
customVariables[row[0]].getTextVariables() :
null,
)}</td>
) }</td>
{ row[1] !== undefined && <td><code>{ row[1] }</code></td> }
</tr>) }
{ otherVariables.map((item, index) =>

View File

@ -18,6 +18,7 @@ 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 { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import { split } from "lodash";
import DMRoomMap from './utils/DMRoomMap';
import { mediaFromMxc } from "./customisations/Media";
@ -122,27 +123,13 @@ export function getInitialLetter(name: string): string {
return undefined;
}
let idx = 0;
const initial = name[0];
if ((initial === '@' || initial === '#' || initial === '+') && name[1]) {
idx++;
name = name.substring(1);
}
// string.codePointAt(0) would do this, but that isn't supported by
// some browsers (notably PhantomJS).
let chars = 1;
const first = name.charCodeAt(idx);
// check if its the start of a surrogate pair
if (first >= 0xD800 && first <= 0xDBFF && name[idx+1]) {
const second = name.charCodeAt(idx+1);
if (second >= 0xDC00 && second <= 0xDFFF) {
chars++;
}
}
const firstChar = name.substring(idx, idx+chars);
return firstChar.toUpperCase();
// rely on the grapheme cluster splitter in lodash so that we don't break apart compound emojis
return split(name, "", 1)[0].toUpperCase();
}
export function avatarUrlForRoom(room: Room, width: number, height: number, resizeMethod?: ResizeMethod) {

View File

@ -56,7 +56,6 @@ limitations under the License.
import React from 'react';
import { MatrixClientPeg } from './MatrixClientPeg';
import PlatformPeg from './PlatformPeg';
import Modal from './Modal';
import { _t } from './languageHandler';
import dis from './dispatcher/dispatcher';
@ -80,7 +79,6 @@ import CountlyAnalytics from "./CountlyAnalytics";
import { UIFeature } from "./settings/UIFeature";
import { CallError } from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker";
import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
@ -99,7 +97,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
// (and store the ID of their native room)
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
export enum AudioID {
enum AudioID {
Ring = 'ringAudio',
Ringback = 'ringbackAudio',
CallEnd = 'callendAudio',
@ -129,19 +127,15 @@ interface ThirdpartyLookupResponse {
fields: ThirdpartyLookupResponseFields;
}
// Unlike 'CallType' in js-sdk, this one includes screen sharing
// (because a screen sharing call is only a screen sharing call to the caller,
// to the callee it's just a video call, at least as far as the current impl
// is concerned).
export enum PlaceCallType {
Voice = 'voice',
Video = 'video',
ScreenSharing = 'screensharing',
}
export enum CallHandlerEvent {
CallsChanged = "calls_changed",
CallChangeRoom = "call_change_room",
SilencedCallsChanged = "silenced_calls_changed",
}
export default class CallHandler extends EventEmitter {
@ -164,6 +158,8 @@ export default class CallHandler extends EventEmitter {
// do the async lookup when we get new information and then store these mappings here
private assertedIdentityNativeUsers = new Map<string, string>();
private silencedCalls = new Set<string>(); // callIds
static sharedInstance() {
if (!window.mxCallHandler) {
window.mxCallHandler = new CallHandler();
@ -224,6 +220,33 @@ export default class CallHandler extends EventEmitter {
}
}
public silenceCall(callId: string) {
this.silencedCalls.add(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
// Don't pause audio if we have calls which are still ringing
if (this.areAnyCallsUnsilenced()) return;
this.pause(AudioID.Ring);
}
public unSilenceCall(callId: string) {
this.silencedCalls.delete(callId);
this.emit(CallHandlerEvent.SilencedCallsChanged, this.silencedCalls);
this.play(AudioID.Ring);
}
public isCallSilenced(callId: string): boolean {
return this.silencedCalls.has(callId);
}
/**
* Returns true if there is at least one unsilenced call
* @returns {boolean}
*/
private areAnyCallsUnsilenced(): boolean {
return this.calls.size > this.silencedCalls.size;
}
private async checkProtocols(maxTries) {
try {
const protocols = await MatrixClientPeg.get().getThirdpartyProtocols();
@ -301,6 +324,13 @@ export default class CallHandler extends EventEmitter {
}, true);
};
public getCallById(callId: string): MatrixCall {
for (const call of this.calls.values()) {
if (call.callId === callId) return call;
}
return null;
}
getCallForRoom(roomId: string): MatrixCall {
return this.calls.get(roomId) || null;
}
@ -441,6 +471,10 @@ export default class CallHandler extends EventEmitter {
break;
}
if (newState !== CallState.Ringing) {
this.silencedCalls.delete(call.callId);
}
switch (newState) {
case CallState.Ringing:
this.play(AudioID.Ring);
@ -450,28 +484,18 @@ export default class CallHandler extends EventEmitter {
break;
case CallState.Ended:
{
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', call.hangupReason);
const hangupReason = call.hangupReason;
Analytics.trackEvent('voip', 'callEnded', 'hangupReason', hangupReason);
this.removeCallForRoom(mappedRoomId);
if (oldState === CallState.InviteSent && (
call.hangupParty === CallParty.Remote ||
(call.hangupParty === CallParty.Local && call.hangupReason === CallErrorCode.InviteTimeout)
)) {
if (oldState === CallState.InviteSent && call.hangupParty === CallParty.Remote) {
this.play(AudioID.Busy);
let title;
let description;
if (call.hangupReason === CallErrorCode.UserHangup) {
title = _t("Call Declined");
description = _t("The other party declined the call.");
} else if (call.hangupReason === CallErrorCode.UserBusy) {
// TODO: We should either do away with these or figure out a copy for each code (expect user_hangup...)
if (call.hangupReason === CallErrorCode.UserBusy) {
title = _t("User Busy");
description = _t("The user you called is busy.");
} 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 {
} else if (hangupReason && ![CallErrorCode.UserHangup, "user hangup"].includes(hangupReason)) {
title = _t("Call Failed");
description = _t("The call could not be established");
}
@ -480,7 +504,7 @@ export default class CallHandler extends EventEmitter {
title, description,
});
} else if (
call.hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
hangupReason === CallErrorCode.AnsweredElsewhere && oldState === CallState.Connecting
) {
Modal.createTrackedDialog('Call Handler', 'Call Failed', ErrorDialog, {
title: _t("Answered Elsewhere"),
@ -615,23 +639,23 @@ export default class CallHandler extends EventEmitter {
private showICEFallbackPrompt() {
const cli = MatrixClientPeg.get();
const code = sub => <code>{sub}</code>;
const code = sub => <code>{ sub }</code>;
Modal.createTrackedDialog('No TURN servers', '', QuestionDialog, {
title: _t("Call failed due to misconfigured server"),
description: <div>
<p>{_t(
<p>{ _t(
"Please ask the administrator of your homeserver " +
"(<code>%(homeserverDomain)s</code>) to configure a TURN server in " +
"order for calls to work reliably.",
{ homeserverDomain: cli.getDomain() }, { code },
)}</p>
<p>{_t(
) }</p>
<p>{ _t(
"Alternatively, you can try to use the public server at " +
"<code>turn.matrix.org</code>, but this will not be as reliable, and " +
"it will share your IP address with that server. You can also manage " +
"this in Settings.",
null, { code },
)}</p>
) }</p>
</div>,
button: _t('Try using turn.matrix.org'),
cancelButton: _t('OK'),
@ -649,19 +673,19 @@ export default class CallHandler extends EventEmitter {
if (call.type === CallType.Voice) {
title = _t("Unable to access microphone");
description = <div>
{_t(
{ _t(
"Call failed because microphone could not be accessed. " +
"Check that a microphone is plugged in and set up correctly.",
)}
) }
</div>;
} else if (call.type === CallType.Video) {
title = _t("Unable to access webcam / microphone");
description = <div>
{_t("Call failed because webcam or microphone could not be accessed. Check that:")}
{ _t("Call failed because webcam or microphone could not be accessed. Check that:") }
<ul>
<li>{_t("A microphone and webcam are plugged in and set up correctly")}</li>
<li>{_t("Permission is granted to use the webcam")}</li>
<li>{_t("No other application is using the webcam")}</li>
<li>{ _t("A microphone and webcam are plugged in and set up correctly") }</li>
<li>{ _t("Permission is granted to use the webcam") }</li>
<li>{ _t("No other application is using the webcam") }</li>
</ul>
</div>;
}
@ -697,25 +721,6 @@ export default class CallHandler extends EventEmitter {
call.placeVoiceCall();
} else if (type === 'video') {
call.placeVideoCall();
} else if (type === PlaceCallType.ScreenSharing) {
const screenCapErrorString = PlatformPeg.get().screenCaptureErrorString();
if (screenCapErrorString) {
this.removeCallForRoom(roomId);
console.log("Can't capture screen: " + screenCapErrorString);
Modal.createTrackedDialog('Call Handler', 'Unable to capture screen', ErrorDialog, {
title: _t('Unable to capture screen'),
description: screenCapErrorString,
});
return;
}
call.placeScreenSharingCall(
async (): Promise<DesktopCapturerSource> => {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
},
);
} else {
console.error("Unknown conf call type: " + type);
}

View File

@ -425,10 +425,10 @@ export default class ContentMessages {
const { finished } = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'),
description: (
<div>{_t(
<div>{ _t(
'At this time it is not possible to reply with a file. ' +
'Would you like to upload this file without replying?',
)}</div>
) }</div>
),
hasCancelButton: true,
button: _t("Continue"),

View File

@ -33,6 +33,7 @@ import { isSecretStorageBeingAccessed, accessSecretStorage } from "./SecurityMan
import { isSecureBackupRequired } from './utils/WellKnownUtils';
import { isLoggedIn } from './components/structures/MatrixChat';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { ActionPayload } from "./dispatcher/payloads";
const KEY_BACKUP_POLL_INTERVAL = 5 * 60 * 1000;
@ -58,28 +59,28 @@ export default class DeviceListener {
}
start() {
MatrixClientPeg.get().on('crypto.willUpdateDevices', this._onWillUpdateDevices);
MatrixClientPeg.get().on('crypto.devicesUpdated', this._onDevicesUpdated);
MatrixClientPeg.get().on('deviceVerificationChanged', this._onDeviceVerificationChanged);
MatrixClientPeg.get().on('userTrustStatusChanged', this._onUserTrustStatusChanged);
MatrixClientPeg.get().on('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().on('accountData', this._onAccountData);
MatrixClientPeg.get().on('sync', this._onSync);
MatrixClientPeg.get().on('RoomState.events', this._onRoomStateEvents);
this.dispatcherRef = dis.register(this._onAction);
this._recheck();
MatrixClientPeg.get().on('crypto.willUpdateDevices', this.onWillUpdateDevices);
MatrixClientPeg.get().on('crypto.devicesUpdated', this.onDevicesUpdated);
MatrixClientPeg.get().on('deviceVerificationChanged', this.onDeviceVerificationChanged);
MatrixClientPeg.get().on('userTrustStatusChanged', this.onUserTrustStatusChanged);
MatrixClientPeg.get().on('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
MatrixClientPeg.get().on('accountData', this.onAccountData);
MatrixClientPeg.get().on('sync', this.onSync);
MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents);
this.dispatcherRef = dis.register(this.onAction);
this.recheck();
}
stop() {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this._onWillUpdateDevices);
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this._onDevicesUpdated);
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this._onDeviceVerificationChanged);
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this._onUserTrustStatusChanged);
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this._onCrossSingingKeysChanged);
MatrixClientPeg.get().removeListener('accountData', this._onAccountData);
MatrixClientPeg.get().removeListener('sync', this._onSync);
MatrixClientPeg.get().removeListener('RoomState.events', this._onRoomStateEvents);
MatrixClientPeg.get().removeListener('crypto.willUpdateDevices', this.onWillUpdateDevices);
MatrixClientPeg.get().removeListener('crypto.devicesUpdated', this.onDevicesUpdated);
MatrixClientPeg.get().removeListener('deviceVerificationChanged', this.onDeviceVerificationChanged);
MatrixClientPeg.get().removeListener('userTrustStatusChanged', this.onUserTrustStatusChanged);
MatrixClientPeg.get().removeListener('crossSigning.keysChanged', this.onCrossSingingKeysChanged);
MatrixClientPeg.get().removeListener('accountData', this.onAccountData);
MatrixClientPeg.get().removeListener('sync', this.onSync);
MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents);
}
if (this.dispatcherRef) {
dis.unregister(this.dispatcherRef);
@ -103,15 +104,15 @@ export default class DeviceListener {
this.dismissed.add(d);
}
this._recheck();
this.recheck();
}
dismissEncryptionSetup() {
this.dismissedThisDeviceToast = true;
this._recheck();
this.recheck();
}
_ensureDeviceIdsAtStartPopulated() {
private ensureDeviceIdsAtStartPopulated() {
if (this.ourDeviceIdsAtStart === null) {
const cli = MatrixClientPeg.get();
this.ourDeviceIdsAtStart = new Set(
@ -120,39 +121,39 @@ export default class DeviceListener {
}
}
_onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
private onWillUpdateDevices = async (users: string[], initialFetch?: boolean) => {
// If we didn't know about *any* devices before (ie. it's fresh login),
// then they are all pre-existing devices, so ignore this and set the
// devicesAtStart list to the devices that we see after the fetch.
if (initialFetch) return;
const myUserId = MatrixClientPeg.get().getUserId();
if (users.includes(myUserId)) this._ensureDeviceIdsAtStartPopulated();
if (users.includes(myUserId)) this.ensureDeviceIdsAtStartPopulated();
// No need to do a recheck here: we just need to get a snapshot of our devices
// before we download any new ones.
};
_onDevicesUpdated = (users: string[]) => {
private onDevicesUpdated = (users: string[]) => {
if (!users.includes(MatrixClientPeg.get().getUserId())) return;
this._recheck();
this.recheck();
};
_onDeviceVerificationChanged = (userId: string) => {
private onDeviceVerificationChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
this.recheck();
};
_onUserTrustStatusChanged = (userId: string) => {
private onUserTrustStatusChanged = (userId: string) => {
if (userId !== MatrixClientPeg.get().getUserId()) return;
this._recheck();
this.recheck();
};
_onCrossSingingKeysChanged = () => {
this._recheck();
private onCrossSingingKeysChanged = () => {
this.recheck();
};
_onAccountData = (ev) => {
private onAccountData = (ev: MatrixEvent) => {
// User may have:
// * migrated SSSS to symmetric
// * uploaded keys to secret storage
@ -163,32 +164,32 @@ export default class DeviceListener {
ev.getType().startsWith('m.cross_signing.') ||
ev.getType() === 'm.megolm_backup.v1'
) {
this._recheck();
this.recheck();
}
};
_onSync = (state, prevState) => {
if (state === 'PREPARED' && prevState === null) this._recheck();
private onSync = (state, prevState) => {
if (state === 'PREPARED' && prevState === null) this.recheck();
};
_onRoomStateEvents = (ev: MatrixEvent) => {
private onRoomStateEvents = (ev: MatrixEvent) => {
if (ev.getType() !== "m.room.encryption") {
return;
}
// If a room changes to encrypted, re-check as it may be our first
// encrypted room. This also catches encrypted room creation as well.
this._recheck();
this.recheck();
};
_onAction = ({ action }) => {
private onAction = ({ action }: ActionPayload) => {
if (action !== "on_logged_in") return;
this._recheck();
this.recheck();
};
// The server doesn't tell us when key backup is set up, so we poll
// & cache the result
async _getKeyBackupInfo() {
private async getKeyBackupInfo() {
const now = (new Date()).getTime();
if (!this.keyBackupInfo || this.keyBackupFetchedAt < now - KEY_BACKUP_POLL_INTERVAL) {
this.keyBackupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
@ -206,7 +207,7 @@ export default class DeviceListener {
return cli && cli.getRooms().some(r => cli.isRoomEncrypted(r.roomId));
}
async _recheck() {
private async recheck() {
const cli = MatrixClientPeg.get();
if (!await cli.doesServerSupportUnstableFeature("org.matrix.e2e_cross_signing")) return;
@ -235,7 +236,7 @@ export default class DeviceListener {
// Cross-signing on account but this device doesn't trust the master key (verify this session)
showSetupEncryptionToast(SetupKind.VERIFY_THIS_SESSION);
} else {
const backupInfo = await this._getKeyBackupInfo();
const backupInfo = await this.getKeyBackupInfo();
if (backupInfo) {
// No cross-signing on account but key backup available (upgrade encryption)
showSetupEncryptionToast(SetupKind.UPGRADE_ENCRYPTION);
@ -256,7 +257,7 @@ export default class DeviceListener {
// This needs to be done after awaiting on downloadKeys() above, so
// we make sure we get the devices after the fetch is done.
this._ensureDeviceIdsAtStartPopulated();
this.ensureDeviceIdsAtStartPopulated();
// Unverified devices that were there last time the app ran
// (technically could just be a boolean: we don't actually

View File

@ -33,7 +33,7 @@ import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import linkifyMatrix from './linkify-matrix';
import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
import { getEmojiFromUnicode } from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread";
import { mediaFromMxc } from "./customisations/Media";
@ -57,7 +57,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i');
const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/;
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'];
export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix'];
const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/;
@ -79,20 +79,8 @@ function mightContainEmoji(str: string): boolean {
* @return {String} The shortcode (such as :thumbup:)
*/
export function unicodeToShortcode(char: string): string {
const data = getEmojiFromUnicode(char);
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
}
/**
* Returns the unicode character for an emoji shortcode
*
* @param {String} shortcode The shortcode (such as :thumbup:)
* @return {String} The emoji character; null if none exists
*/
export function shortcodeToUnicode(shortcode: string): string {
shortcode = shortcode.slice(1, shortcode.length - 1);
const data = SHORTCODE_TO_EMOJI.get(shortcode);
return data ? data.unicode : null;
const shortcodes = getEmojiFromUnicode(char)?.shortcodes;
return shortcodes?.length ? `:${shortcodes[0]}:` : '';
}
export function processHtmlForSending(html: string): string {

View File

@ -149,17 +149,17 @@ export default class IdentityAuthClient {
title: _t("Identity server has no terms of service"),
description: (
<div>
<p>{_t(
<p>{ _t(
"This action requires accessing the default identity server " +
"<server /> to validate an email address or phone number, " +
"but the server does not have any terms of service.", {},
{
server: () => <b>{abbreviateUrl(identityServerUrl)}</b>,
server: () => <b>{ abbreviateUrl(identityServerUrl) }</b>,
},
)}</p>
<p>{_t(
) }</p>
<p>{ _t(
"Only continue if you trust the owner of the server.",
)}</p>
) }</p>
</div>
),
button: _t("Trust"),

View File

@ -105,7 +105,7 @@ export interface IMatrixClientPeg {
* This module provides a singleton instance of this class so the 'current'
* Matrix Client object is available easily.
*/
class _MatrixClientPeg implements IMatrixClientPeg {
class MatrixClientPegClass implements IMatrixClientPeg {
// These are the default options used when when the
// client is started in 'start'. These can be altered
// at any time up to after the 'will_start_client'
@ -300,7 +300,7 @@ class _MatrixClientPeg implements IMatrixClientPeg {
}
if (!window.mxMatrixClientPeg) {
window.mxMatrixClientPeg = new _MatrixClientPeg();
window.mxMatrixClientPeg = new MatrixClientPegClass();
}
export const MatrixClientPeg = window.mxMatrixClientPeg;

View File

@ -378,7 +378,7 @@ export class ModalManager {
const dialog = (
<div className={classes}>
<div className="mx_Dialog">
{modal.elem}
{ modal.elem }
</div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
</div>

View File

@ -51,10 +51,15 @@ export async function startAnyRegistrationFlow(options) {
description: _t("Use your account or create a new one to continue."),
button: _t("Create Account"),
extraButtons: [
<button key="start_login" onClick={() => {
modal.close();
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
}}>{ _t('Sign In') }</button>,
<button
key="start_login"
onClick={() => {
modal.close();
dis.dispatch({ action: 'start_login', screenAfterLogin: options.screen_after });
}}
>
{ _t('Sign In') }
</button>,
],
onFinished: (proceed) => {
if (proceed) {

View File

@ -34,7 +34,6 @@ import { getAddressType } from './UserAddress';
import { abbreviateUrl } from './utils/UrlUtils';
import { getDefaultIdentityServerUrl, useDefaultIdentityServer } from './utils/IdentityServerUtils';
import { isPermalinkHost, parsePermalink } from "./utils/permalinks/Permalinks";
import { inviteUsersToRoom } from "./RoomInvite";
import { WidgetType } from "./widgets/WidgetType";
import { Jitsi } from "./widgets/Jitsi";
import { parseFragment as parseHtml, Element as ChildElement } from "parse5";
@ -49,6 +48,7 @@ import { UIFeature } from "./settings/UIFeature";
import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms";
import { upgradeRoom } from './utils/RoomUpgrade';
import UploadConfirmDialog from './components/views/dialogs/UploadConfirmDialog';
import ErrorDialog from './components/views/dialogs/ErrorDialog';
import DevtoolsDialog from './components/views/dialogs/DevtoolsDialog';
@ -277,50 +277,8 @@ export const Commands = [
/*isPriority=*/false, /*isStatic=*/true);
return success(finished.then(async ([resp]) => {
if (!resp.continue) return;
let checkForUpgradeFn;
try {
const upgradePromise = cli.upgradeRoom(roomId, args);
// We have to wait for the js-sdk to give us the room back so
// we can more effectively abuse the MultiInviter behaviour
// which heavily relies on the Room object being available.
if (resp.invite) {
checkForUpgradeFn = async (newRoom) => {
// The upgradePromise should be done by the time we await it here.
const { replacement_room: newRoomId } = await upgradePromise;
if (newRoom.roomId !== newRoomId) return;
const toInvite = [
...room.getMembersWithMembership("join"),
...room.getMembersWithMembership("invite"),
].map(m => m.userId).filter(m => m !== cli.getUserId());
if (toInvite.length > 0) {
// Errors are handled internally to this function
await inviteUsersToRoom(newRoomId, toInvite);
}
cli.removeListener('Room', checkForUpgradeFn);
};
cli.on('Room', checkForUpgradeFn);
}
// We have to await after so that the checkForUpgradesFn has a proper reference
// to the new room's ID.
await upgradePromise;
} catch (e) {
console.error(e);
if (checkForUpgradeFn) cli.removeListener('Room', checkForUpgradeFn);
Modal.createTrackedDialog('Slash Commands', 'room upgrade error', ErrorDialog, {
title: _t('Error upgrading room'),
description: _t(
'Double check that your server supports the room version chosen and try again.'),
});
}
if (!resp?.continue) return;
await upgradeRoom(room, args, resp.invite);
}));
}
return reject(this.getUsage());
@ -480,14 +438,14 @@ export const Commands = [
'Identity server',
QuestionDialog, {
title: _t("Use an identity server"),
description: <p>{_t(
description: <p>{ _t(
"Use an identity server to invite by email. " +
"Click continue to use the default identity server " +
"(%(defaultIdentityServerName)s) or manage in Settings.",
{
defaultIdentityServerName: abbreviateUrl(defaultIdentityServerUrl),
},
)}</p>,
) }</p>,
button: _t("Continue"),
},
);
@ -522,7 +480,7 @@ export const Commands = [
aliases: ['j', 'goto'],
args: '<room-address>',
description: _td('Joins room with given address'),
runFn: function(_, args) {
runFn: function(roomId, args) {
if (args) {
// Note: we support 2 versions of this command. The first is
// the public-facing one for most users and the other is a
@ -1069,7 +1027,7 @@ export const Commands = [
command: "msg",
description: _td("Sends a message to the given user"),
args: "<user-id> <message>",
runFn: function(_, args) {
runFn: function(roomId, args) {
if (args) {
// matches the first whitespace delimited group and then the rest of the string
const matches = args.match(/^(\S+?)(?: +(.*))?$/s);

View File

@ -14,7 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { MatrixClientPeg } from './MatrixClientPeg';
import { _t } from './languageHandler';
import * as Roles from './Roles';
import { isValid3pidInvite } from "./RoomInvite";
@ -318,90 +317,6 @@ function textForCanonicalAliasEvent(ev: MatrixEvent): () => string | null {
});
}
function textForCallAnswerEvent(event: MatrixEvent): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone');
const supported = MatrixClientPeg.get().supportsVoip() ? '' : _t('(not supported by this browser)');
return _t('%(senderName)s answered the call.', { senderName }) + ' ' + supported;
};
}
function textForCallHangupEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
const eventContent = event.getContent();
let getReason = () => "";
if (!MatrixClientPeg.get().supportsVoip()) {
getReason = () => _t('(not supported by this browser)');
} else if (eventContent.reason) {
if (eventContent.reason === "ice_failed") {
// We couldn't establish a connection at all
getReason = () => _t('(could not connect media)');
} else if (eventContent.reason === "ice_timeout") {
// We established a connection but it died
getReason = () => _t('(connection failed)');
} else if (eventContent.reason === "user_media_failed") {
// The other side couldn't open capture devices
getReason = () => _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)
getReason = () => _t("(an error occurred)");
} else if (eventContent.reason === "invite_timeout") {
getReason = () => _t('(no answer)');
} else if (eventContent.reason === "user hangup" || eventContent.reason === "user_hangup") {
// workaround for https://github.com/vector-im/element-web/issues/5178
// it seems Android randomly sets a reason of "user hangup" which is
// interpreted as an error code :(
// https://github.com/vector-im/riot-android/issues/2623
// Also the correct hangup code as of VoIP v1 (with underscore)
getReason = () => '';
} else {
getReason = () => _t('(unknown failure: %(reason)s)', { reason: eventContent.reason });
}
}
return () => _t('%(senderName)s ended the call.', { senderName: getSenderName() }) + ' ' + getReason();
}
function textForCallRejectEvent(event: MatrixEvent): () => string | null {
return () => {
const senderName = event.sender ? event.sender.name : _t('Someone');
return _t('%(senderName)s declined the call.', { senderName });
};
}
function textForCallInviteEvent(event: MatrixEvent): () => string | null {
const getSenderName = () => event.sender ? event.sender.name : _t('Someone');
// FIXME: Find a better way to determine this from the event?
let isVoice = true;
if (event.getContent().offer && event.getContent().offer.sdp &&
event.getContent().offer.sdp.indexOf('m=video') !== -1) {
isVoice = false;
}
const isSupported = MatrixClientPeg.get().supportsVoip();
// This ladder could be reduced down to a couple string variables, however other languages
// can have a hard time translating those strings. In an effort to make translations easier
// and more accurate, we break out the string-based variables to a couple booleans.
if (isVoice && isSupported) {
return () => _t("%(senderName)s placed a voice call.", {
senderName: getSenderName(),
});
} else if (isVoice && !isSupported) {
return () => _t("%(senderName)s placed a voice call. (not supported by this browser)", {
senderName: getSenderName(),
});
} else if (!isVoice && isSupported) {
return () => _t("%(senderName)s placed a video call.", {
senderName: getSenderName(),
});
} else if (!isVoice && !isSupported) {
return () => _t("%(senderName)s placed a video call. (not supported by this browser)", {
senderName: getSenderName(),
});
}
}
function textForThreePidInviteEvent(event: MatrixEvent): () => string | null {
const senderName = event.sender ? event.sender.name : event.getSender();
@ -652,10 +567,6 @@ interface IHandlers {
const handlers: IHandlers = {
'm.room.message': textForMessageEvent,
'm.call.invite': textForCallInviteEvent,
'm.call.answer': textForCallAnswerEvent,
'm.call.hangup': textForCallHangupEvent,
'm.call.reject': textForCallRejectEvent,
};
const stateHandlers: IHandlers = {

View File

@ -14,35 +14,33 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import PropTypes from "prop-types";
const emailRegex = /^\S+@\S+\.\S+$/;
const mxUserIdRegex = /^@\S+:\S+$/;
const mxRoomIdRegex = /^!\S+:\S+$/;
export const addressTypes = ['mx-user-id', 'mx-room-id', 'email'];
export enum AddressType {
Email = "email",
MatrixUserId = "mx-user-id",
MatrixRoomId = "mx-room-id",
}
export const addressTypes = [AddressType.Email, AddressType.MatrixRoomId, AddressType.MatrixUserId];
// PropType definition for an object describing
// an address that can be invited to a room (which
// could be a third party identifier or a matrix ID)
// along with some additional information about the
// address / target.
export const UserAddressType = PropTypes.shape({
addressType: PropTypes.oneOf(addressTypes).isRequired,
address: PropTypes.string.isRequired,
displayName: PropTypes.string,
avatarMxc: PropTypes.string,
export interface IUserAddress {
addressType: AddressType;
address: string;
displayName?: string;
avatarMxc?: string;
// true if the address is known to be a valid address (eg. is a real
// user we've seen) or false otherwise (eg. is just an address the
// user has entered)
isKnown: PropTypes.bool,
});
isKnown?: boolean;
}
export function getAddressType(inputText: string): AddressType | null {
if (emailRegex.test(inputText)) {

View File

@ -370,8 +370,8 @@ export const toggleDialog = () => {
const sections = categoryOrder.map(category => {
const list = shortcuts[category];
return <div className="mx_KeyboardShortcutsDialog_category" key={category}>
<h3>{_t(category)}</h3>
<div>{list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />)}</div>
<h3>{ _t(category) }</h3>
<div>{ list.map(shortcut => <Shortcut key={shortcut.description} shortcut={shortcut} />) }</div>
</div>;
});

View File

@ -62,9 +62,9 @@ const Toolbar: React.FC<IProps> = ({ children, ...props }) => {
};
return <RovingTabIndexProvider handleHomeEnd={true} onKeyDown={onKeyDown}>
{({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
{ ({ onKeyDownHandler }) => <div {...props} onKeyDown={onKeyDownHandler} role="toolbar">
{ children }
</div>}
</div> }
</RovingTabIndexProvider>;
};

View File

@ -15,8 +15,10 @@ limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../../index';
import PropTypes from 'prop-types';
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import Spinner from "../../../../components/views/elements/Spinner";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from '../../../../languageHandler';
@ -24,46 +26,44 @@ import SettingsStore from "../../../../settings/SettingsStore";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import { Action } from "../../../../dispatcher/actions";
import { SettingLevel } from "../../../../settings/SettingLevel";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
disabling: boolean;
}
/*
* Allows the user to disable the Event Index.
*/
export default class DisableEventIndexDialog extends React.Component {
static propTypes = {
onFinished: PropTypes.func.isRequired,
}
constructor(props) {
export default class DisableEventIndexDialog extends React.Component<IProps, IState> {
constructor(props: IProps) {
super(props);
this.state = {
disabling: false,
};
}
_onDisable = async () => {
private onDisable = async (): Promise<void> => {
this.setState({
disabling: true,
});
await SettingsStore.setValue('enableEventIndexing', null, SettingLevel.DEVICE, false);
await EventIndexPeg.deleteEventIndex();
this.props.onFinished();
this.props.onFinished(true);
dis.fire(Action.ViewUserSettings);
}
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const Spinner = sdk.getComponent('elements.Spinner');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
};
public render(): React.ReactNode {
return (
<BaseDialog onFinished={this.props.onFinished} title={_t("Are you sure?")}>
{_t("If disabled, messages from encrypted rooms won't appear in search results.")}
{this.state.disabling ? <Spinner /> : <div />}
{ _t("If disabled, messages from encrypted rooms won't appear in search results.") }
{ this.state.disabling ? <Spinner /> : <div /> }
<DialogButtons
primaryButton={_t('Disable')}
onPrimaryButtonClick={this._onDisable}
onPrimaryButtonClick={this.onDisable}
primaryButtonClass="danger"
cancelButtonClass="warning"
onCancel={this.props.onFinished}

View File

@ -134,8 +134,9 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
}
private onDisable = async () => {
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
import("./DisableEventIndexDialog"),
const DisableEventIndexDialog = (await import("./DisableEventIndexDialog")).default;
Modal.createTrackedDialog("Disable message search", "Disable message search",
DisableEventIndexDialog,
null, null, /* priority = */ false, /* static = */ true,
);
};
@ -161,19 +162,19 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
const eventIndexingSettings = (
<div>
{_t(
{ _t(
"%(brand)s is securely caching encrypted messages locally for them " +
"to appear in search results:",
{ brand },
)}
) }
<div className='mx_SettingsTab_subsectionText'>
{crawlerState}<br />
{_t("Space used:")} {formatBytes(this.state.eventIndexSize, 0)}<br />
{_t("Indexed messages:")} {formatCountLong(this.state.eventCount)}<br />
{_t("Indexed rooms:")} {_t("%(doneRooms)s out of %(totalRooms)s", {
{ crawlerState }<br />
{ _t("Space used:") } { formatBytes(this.state.eventIndexSize, 0) }<br />
{ _t("Indexed messages:") } { formatCountLong(this.state.eventCount) }<br />
{ _t("Indexed rooms:") } { _t("%(doneRooms)s out of %(totalRooms)s", {
doneRooms: formatCountLong(doneRooms),
totalRooms: formatCountLong(this.state.roomCount),
})} <br />
}) } <br />
<Field
label={_t('Message downloading sleep time(ms)')}
type='number'
@ -188,7 +189,7 @@ export default class ManageEventIndexDialog extends React.Component<IProps, ISta
onFinished={this.props.onFinished}
title={_t("Message search")}
>
{eventIndexingSettings}
{ eventIndexingSettings }
<DialogButtons
primaryButton={_t("Done")}
onPrimaryButtonClick={this.props.onFinished}

View File

@ -232,15 +232,15 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t(
<p>{ _t(
"<b>Warning</b>: You should only set up key backup from a trusted computer.", {},
{ b: sub => <b>{sub}</b> },
)}</p>
<p>{_t(
{ b: sub => <b>{ sub }</b> },
) }</p>
<p>{ _t(
"We'll store an encrypted copy of your keys on our server. " +
"Secure your backup with a Security Phrase.",
)}</p>
<p>{_t("For maximum security, this should be different from your account password.")}</p>
) }</p>
<p>{ _t("For maximum security, this should be different from your account password.") }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
@ -268,9 +268,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
/>
<details>
<summary>{_t("Advanced")}</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick} >
{_t("Set up with a Security Key")}
<summary>{ _t("Advanced") }</summary>
<AccessibleButton kind='primary' onClick={this._onSkipPassPhraseClick}>
{ _t("Set up with a Security Key") }
</AccessibleButton>
</details>
</form>;
@ -299,19 +299,19 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
<div>{matchText}</div>
<div>{ matchText }</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText}
{ changeText }
</AccessibleButton>
</div>
</div>;
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
<p>{ _t(
"Enter your Security Phrase a second time to confirm it.",
)}</p>
) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_passPhraseContainer">
<div>
@ -323,7 +323,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
autoFocus={true}
/>
</div>
{passPhraseMatch}
{ passPhraseMatch }
</div>
</div>
<DialogButtons
@ -337,27 +337,27 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseShowKey() {
return <div>
<p>{_t(
<p>{ _t(
"Your Security Key is a safety net - you can use it to restore " +
"access to your encrypted messages if you forget your Security Phrase.",
)}</p>
<p>{_t(
) }</p>
<p>{ _t(
"Keep a copy of it somewhere secure, like a password manager or even a safe.",
)}</p>
) }</p>
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Security Key")}
{ _t("Your Security Key") }
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._keyBackupInfo.recovery_key}</code>
<code ref={this._collectRecoveryKeyNode}>{ this._keyBackupInfo.recovery_key }</code>
</div>
<div className="mx_CreateKeyBackupDialog_recoveryKeyButtons">
<button className="mx_Dialog_primary" onClick={this._onCopyClick}>
{_t("Copy")}
{ _t("Copy") }
</button>
<button className="mx_Dialog_primary" onClick={this._onDownloadClick}>
{_t("Download")}
{ _t("Download") }
</button>
</div>
</div>
@ -370,26 +370,26 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
if (this.state.copied) {
introText = _t(
"Your Security Key has been <b>copied to your clipboard</b>, paste it to:",
{}, { b: s => <b>{s}</b> },
{}, { b: s => <b>{ s }</b> },
);
} else if (this.state.downloaded) {
introText = _t(
"Your Security Key is in your <b>Downloads</b> folder.",
{}, { b: s => <b>{s}</b> },
{}, { b: s => <b>{ s }</b> },
);
}
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{introText}
{ introText }
<ul>
<li>{_t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{s}</b> })}</li>
<li>{_t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{s}</b> })}</li>
<li>{_t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{s}</b> })}</li>
<li>{ _t("<b>Print it</b> and store it somewhere safe", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{ _t("<b>Save it</b> on a USB key or backup drive", {}, { b: s => <b>{ s }</b> }) }</li>
<li>{ _t("<b>Copy it</b> to your personal cloud storage", {}, { b: s => <b>{ s }</b> }) }</li>
</ul>
<DialogButtons primaryButton={_t("Continue")}
onPrimaryButtonClick={this._createBackup}
hasCancel={false}>
<button onClick={this._onKeepItSafeBackClick}>{_t("Back")}</button>
<button onClick={this._onKeepItSafeBackClick}>{ _t("Back") }</button>
</DialogButtons>
</div>;
}
@ -404,9 +404,9 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseDone() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t(
<p>{ _t(
"Your keys are being backed up (the first backup could take a few minutes).",
)}</p>
) }</p>
<DialogButtons primaryButton={_t('OK')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
@ -417,10 +417,10 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
_renderPhaseOptOutConfirm() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
{_t(
{ _t(
"Without setting up Secure Message Recovery, you won't be able to restore your " +
"encrypted message history if you log out or use another session.",
)}
) }
<DialogButtons primaryButton={_t('Set up Secure Message Recovery')}
onPrimaryButtonClick={this._onSetUpClick}
hasCancel={false}
@ -457,7 +457,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
if (this.state.error) {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
content = <div>
<p>{_t("Unable to create key backup")}</p>
<p>{ _t("Unable to create key backup") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._createBackup}
@ -499,7 +499,7 @@ export default class CreateKeyBackupDialog extends React.PureComponent {
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
>
<div>
{content}
{ content }
</div>
</BaseDialog>
);

View File

@ -474,10 +474,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup"></span>
{_t("Generate a Security Key")}
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_secureBackup" />
{ _t("Generate a Security Key") }
</div>
<div>{_t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
<div>{ _t("Well generate a Security Key for you to store somewhere safe, like a password manager or a safe.") }</div>
</StyledRadioButton>
);
}
@ -493,10 +493,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
outlined
>
<div className="mx_CreateSecretStorageDialog_optionTitle">
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase"></span>
{_t("Enter a Security Phrase")}
<span className="mx_CreateSecretStorageDialog_optionIcon mx_CreateSecretStorageDialog_optionIcon_securePhrase" />
{ _t("Enter a Security Phrase") }
</div>
<div>{_t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.")}</div>
<div>{ _t("Use a secret phrase only you know, and optionally save a Security Key to use for backup.") }</div>
</StyledRadioButton>
);
}
@ -507,13 +507,13 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
const optionPassphrase = setupMethods.includes("passphrase") ? this._renderOptionPassphrase() : null;
return <form onSubmit={this._onChooseKeyPassphraseFormSubmit}>
<p className="mx_CreateSecretStorageDialog_centeredBody">{_t(
<p className="mx_CreateSecretStorageDialog_centeredBody">{ _t(
"Safeguard against losing access to encrypted messages & data by " +
"backing up encryption keys on your server.",
)}</p>
) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer" role="radiogroup">
{optionKey}
{optionPassphrase}
{ optionKey }
{ optionPassphrase }
</div>
<DialogButtons
primaryButton={_t("Continue")}
@ -536,7 +536,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let nextCaption = _t("Next");
if (this.state.canUploadKeysWithPasswordOnly) {
authPrompt = <div>
<div>{_t("Enter your account password to confirm the upgrade:")}</div>
<div>{ _t("Enter your account password to confirm the upgrade:") }</div>
<div><Field
type="password"
label={_t("Password")}
@ -548,22 +548,22 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>;
} else if (!this.state.backupSigStatus.usable) {
authPrompt = <div>
<div>{_t("Restore your key backup to upgrade your encryption")}</div>
<div>{ _t("Restore your key backup to upgrade your encryption") }</div>
</div>;
nextCaption = _t("Restore");
} else {
authPrompt = <p>
{_t("You'll need to authenticate with the server to confirm the upgrade.")}
{ _t("You'll need to authenticate with the server to confirm the upgrade.") }
</p>;
}
return <form onSubmit={this._onMigrateFormSubmit}>
<p>{_t(
<p>{ _t(
"Upgrade this session to allow it to verify other sessions, " +
"granting them access to encrypted messages and marking them " +
"as trusted for other users.",
)}</p>
<div>{authPrompt}</div>
) }</p>
<div>{ authPrompt }</div>
<DialogButtons
primaryButton={nextCaption}
onPrimaryButtonClick={this._onMigrateFormSubmit}
@ -571,7 +571,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
primaryDisabled={this.state.canUploadKeysWithPasswordOnly && !this.state.accountPassword}
>
<button type="button" className="danger" onClick={this._onCancelClick}>
{_t('Skip')}
{ _t('Skip') }
</button>
</DialogButtons>
</form>;
@ -579,10 +579,10 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhasePassPhrase() {
return <form onSubmit={this._onPassPhraseNextClick}>
<p>{_t(
<p>{ _t(
"Enter a security phrase only you know, as its used to safeguard your data. " +
"To be secure, you shouldnt re-use your account password.",
)}</p>
) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<PassphraseField
@ -609,7 +609,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<button type="button"
onClick={this._onCancelClick}
className="danger"
>{_t("Cancel")}</button>
>{ _t("Cancel") }</button>
</DialogButtons>
</form>;
}
@ -637,18 +637,18 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let passPhraseMatch = null;
if (matchText) {
passPhraseMatch = <div>
<div>{matchText}</div>
<div>{ matchText }</div>
<div>
<AccessibleButton element="span" className="mx_linkButton" onClick={this._onSetAgainClick}>
{changeText}
{ changeText }
</AccessibleButton>
</div>
</div>;
}
return <form onSubmit={this._onPassPhraseConfirmNextClick}>
<p>{_t(
<p>{ _t(
"Enter your Security Phrase a second time to confirm it.",
)}</p>
) }</p>
<div className="mx_CreateSecretStorageDialog_passPhraseContainer">
<Field
type="password"
@ -660,7 +660,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
autoComplete="new-password"
/>
<div className="mx_CreateSecretStorageDialog_passPhraseMatch">
{passPhraseMatch}
{ passPhraseMatch }
</div>
</div>
<DialogButtons
@ -672,7 +672,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
<button type="button"
onClick={this._onCancelClick}
className="danger"
>{_t("Skip")}</button>
>{ _t("Skip") }</button>
</DialogButtons>
</form>;
}
@ -691,35 +691,36 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
</div>;
}
return <div>
<p>{_t(
<p>{ _t(
"Store your Security Key somewhere safe, like a password manager or a safe, " +
"as its used to safeguard your encrypted data.",
)}</p>
) }</p>
<div className="mx_CreateSecretStorageDialog_primaryContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKeyContainer">
<div className="mx_CreateSecretStorageDialog_recoveryKey">
<code ref={this._collectRecoveryKeyNode}>{this._recoveryKey.encodedPrivateKey}</code>
<code ref={this._collectRecoveryKeyNode}>{ this._recoveryKey.encodedPrivateKey }</code>
</div>
<div className="mx_CreateSecretStorageDialog_recoveryKeyButtons">
<AccessibleButton kind='primary' className="mx_Dialog_primary"
<AccessibleButton kind='primary'
className="mx_Dialog_primary"
onClick={this._onDownloadClick}
disabled={this.state.phase === PHASE_STORING}
>
{_t("Download")}
{ _t("Download") }
</AccessibleButton>
<span>{_t("or")}</span>
<span>{ _t("or") }</span>
<AccessibleButton
kind='primary'
className="mx_Dialog_primary mx_CreateSecretStorageDialog_recoveryKeyButtons_copyBtn"
onClick={this._onCopyClick}
disabled={this.state.phase === PHASE_STORING}
>
{this.state.copied ? _t("Copied!") : _t("Copy")}
{ this.state.copied ? _t("Copied!") : _t("Copy") }
</AccessibleButton>
</div>
</div>
</div>
{continueButton}
{ continueButton }
</div>;
}
@ -732,7 +733,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhaseLoadError() {
return <div>
<p>{_t("Unable to query secret storage status")}</p>
<p>{ _t("Unable to query secret storage status") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._onLoadRetryClick}
@ -745,17 +746,17 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
_renderPhaseSkipConfirm() {
return <div>
<p>{_t(
<p>{ _t(
"If you cancel now, you may lose encrypted messages & data if you lose access to your logins.",
)}</p>
<p>{_t(
) }</p>
<p>{ _t(
"You can also set up Secure Backup & manage your keys in Settings.",
)}</p>
) }</p>
<DialogButtons primaryButton={_t('Go back')}
onPrimaryButtonClick={this._onGoBackClick}
hasCancel={false}
>
<button type="button" className="danger" onClick={this._onCancel}>{_t('Cancel')}</button>
<button type="button" className="danger" onClick={this._onCancel}>{ _t('Cancel') }</button>
</DialogButtons>
</div>;
}
@ -787,7 +788,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
let content;
if (this.state.error) {
content = <div>
<p>{_t("Unable to set up secret storage")}</p>
<p>{ _t("Unable to set up secret storage") }</p>
<div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapSecretStorage}
@ -857,7 +858,7 @@ export default class CreateSecretStorageDialog extends React.PureComponent {
fixedWidth={false}
>
<div>
{content}
{ content }
</div>
</BaseDialog>
);

View File

@ -148,8 +148,12 @@ export default class ExportE2eKeysDialog extends React.Component {
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase1} id='passphrase1'
autoFocus={true} size='64' type='password'
<input
ref={this._passphrase1}
id='passphrase1'
autoFocus={true}
size='64'
type='password'
disabled={disableForm}
/>
</div>
@ -161,8 +165,10 @@ export default class ExportE2eKeysDialog extends React.Component {
</label>
</div>
<div className='mx_E2eKeysDialog_inputCell'>
<input ref={this._passphrase2} id='passphrase2'
size='64' type='password'
<input ref={this._passphrase2}
id='passphrase2'
size='64'
type='password'
disabled={disableForm}
/>
</div>

View File

@ -174,7 +174,10 @@ export default class ImportE2eKeysDialog extends React.Component {
</div>
</div>
<div className='mx_Dialog_buttons'>
<input className='mx_Dialog_primary' type='submit' value={_t('Import')}
<input
className='mx_Dialog_primary'
type='submit'
value={_t('Import')}
disabled={!this.state.enableSubmit || disableForm}
/>
<button onClick={this._onCancelClick} disabled={disableForm}>

View File

@ -54,28 +54,28 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_KeyBackupFailedDialog_title">
{_t("New Recovery Method")}
{ _t("New Recovery Method") }
</span>;
const newMethodDetected = <p>{_t(
const newMethodDetected = <p>{ _t(
"A new Security Phrase and key for Secure Messages have been detected.",
)}</p>;
) }</p>;
const hackWarning = <p className="warning">{_t(
const hackWarning = <p className="warning">{ _t(
"If you didn't set the new recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
)}</p>;
) }</p>;
let content;
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
content = <div>
{newMethodDetected}
<p>{_t(
{ newMethodDetected }
<p>{ _t(
"This session is encrypting history using the new recovery method.",
)}</p>
{hackWarning}
) }</p>
{ hackWarning }
<DialogButtons
primaryButton={_t("OK")}
onPrimaryButtonClick={this.onOkClick}
@ -85,8 +85,8 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
</div>;
} else {
content = <div>
{newMethodDetected}
{hackWarning}
{ newMethodDetected }
{ hackWarning }
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick}
@ -101,7 +101,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
onFinished={this.props.onFinished}
title={title}
>
{content}
{ content }
</BaseDialog>
);
}

View File

@ -46,7 +46,7 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
const title = <span className="mx_KeyBackupFailedDialog_title">
{_t("Recovery Method Removed")}
{ _t("Recovery Method Removed") }
</span>;
return (
@ -55,21 +55,21 @@ export default class RecoveryMethodRemovedDialog extends React.PureComponent {
title={title}
>
<div>
<p>{_t(
<p>{ _t(
"This session has detected that your Security Phrase and key " +
"for Secure Messages have been removed.",
)}</p>
<p>{_t(
) }</p>
<p>{ _t(
"If you did this accidentally, you can setup Secure Messages on " +
"this session which will re-encrypt this session's message " +
"history with a new recovery method.",
)}</p>
<p className="warning">{_t(
) }</p>
<p className="warning">{ _t(
"If you didn't remove the recovery method, an " +
"attacker may be trying to access your account. " +
"Change your account password and set a new recovery " +
"method immediately in Settings.",
)}</p>
) }</p>
<DialogButtons
primaryButton={_t("Set up Secure Messages")}
onPrimaryButtonClick={this.onSetupClick}

View File

@ -0,0 +1,37 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DEFAULT_WAVEFORM, Playback } from "./Playback";
import { PlaybackManager } from "./PlaybackManager";
/**
* A managed playback is a Playback instance that is guided by a PlaybackManager.
*/
export class ManagedPlayback extends Playback {
public constructor(private manager: PlaybackManager, buf: ArrayBuffer, seedWaveform = DEFAULT_WAVEFORM) {
super(buf, seedWaveform);
}
public async play(): Promise<void> {
this.manager.playOnly(this);
return super.play();
}
public destroy() {
this.manager.destroyPlaybackInstance(this);
super.destroy();
}
}

View File

@ -32,7 +32,7 @@ export enum PlaybackState {
export const PLAYBACK_WAVEFORM_SAMPLES = 39;
const THUMBNAIL_WAVEFORM_SAMPLES = 100; // arbitrary: [30,120]
const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
export const DEFAULT_WAVEFORM = arraySeed(0, PLAYBACK_WAVEFORM_SAMPLES);
function makePlaybackWaveform(input: number[]): number[] {
// First, convert negative amplitudes to positive so we don't detect zero as "noisy".
@ -59,9 +59,10 @@ export class Playback extends EventEmitter implements IDestroyable {
public readonly thumbnailWaveform: number[];
private readonly context: AudioContext;
private source: AudioBufferSourceNode;
private source: AudioBufferSourceNode | MediaElementAudioSourceNode;
private state = PlaybackState.Decoding;
private audioBuf: AudioBuffer;
private element: HTMLAudioElement;
private resampledWaveform: number[];
private waveformObservable = new SimpleObservable<number[]>();
private readonly clock: PlaybackClock;
@ -129,36 +130,64 @@ export class Playback extends EventEmitter implements IDestroyable {
this.removeAllListeners();
this.clock.destroy();
this.waveformObservable.close();
if (this.element) {
URL.revokeObjectURL(this.element.src);
this.element.remove();
}
}
public async prepare() {
// Safari compat: promise API not supported on this function
this.audioBuf = await new Promise((resolve, reject) => {
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
// very well.
console.error("Error decoding recording: ", e);
console.warn("Trying to re-encode to WAV instead...");
// The point where we use an audio element is fairly arbitrary, though we don't want
// it to be too low. As of writing, voice messages want to show a waveform but audio
// messages do not. Using an audio element means we can't show a waveform preview, so
// we try to target the difference between a voice message file and large audio file.
// Overall, the point of this is to avoid memory-related issues due to storing a massive
// audio buffer in memory, as that can balloon to far greater than the input buffer's
// byte length.
if (this.buf.byteLength > 5 * 1024 * 1024) { // 5mb
console.log("Audio file too large: processing through <audio /> element");
this.element = document.createElement("AUDIO") as HTMLAudioElement;
const prom = new Promise((resolve, reject) => {
this.element.onloadeddata = () => resolve(null);
this.element.onerror = (e) => reject(e);
});
this.element.src = URL.createObjectURL(new Blob([this.buf]));
await prom; // make sure the audio element is ready for us
} else {
// Safari compat: promise API not supported on this function
this.audioBuf = await new Promise((resolve, reject) => {
this.context.decodeAudioData(this.buf, b => resolve(b), async e => {
try {
// This error handler is largely for Safari as well, which doesn't support Opus/Ogg
// very well.
console.error("Error decoding recording: ", e);
console.warn("Trying to re-encode to WAV instead...");
const wav = await decodeOgg(this.buf);
const wav = await decodeOgg(this.buf);
// noinspection ES6MissingAwait - not needed when using callbacks
this.context.decodeAudioData(wav, b => resolve(b), e => {
console.error("Still failed to decode recording: ", e);
reject(e);
// noinspection ES6MissingAwait - not needed when using callbacks
this.context.decodeAudioData(wav, b => resolve(b), e => {
console.error("Still failed to decode recording: ", e);
reject(e);
});
} catch (e) {
console.error("Caught decoding error:", e);
reject(e);
}
});
});
});
// Update the waveform to the real waveform once we have channel data to use. We don't
// exactly trust the user-provided waveform to be accurate...
const waveform = Array.from(this.audioBuf.getChannelData(0));
this.resampledWaveform = makePlaybackWaveform(waveform);
// Update the waveform to the real waveform once we have channel data to use. We don't
// exactly trust the user-provided waveform to be accurate...
const waveform = Array.from(this.audioBuf.getChannelData(0));
this.resampledWaveform = makePlaybackWaveform(waveform);
}
this.waveformObservable.update(this.resampledWaveform);
this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore
this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update
this.clock.durationSeconds = this.audioBuf.duration;
this.clock.durationSeconds = this.element ? this.element.duration : this.audioBuf.duration;
}
private onPlaybackEnd = async () => {
@ -171,7 +200,11 @@ export class Playback extends EventEmitter implements IDestroyable {
if (this.state === PlaybackState.Stopped) {
this.disconnectSource();
this.makeNewSourceBuffer();
this.source.start();
if (this.element) {
await this.element.play();
} else {
(this.source as AudioBufferSourceNode).start();
}
}
// We use the context suspend/resume functions because it allows us to pause a source
@ -182,13 +215,21 @@ export class Playback extends EventEmitter implements IDestroyable {
}
private disconnectSource() {
if (this.element) return; // leave connected, we can (and must) re-use it
this.source?.disconnect();
this.source?.removeEventListener("ended", this.onPlaybackEnd);
}
private makeNewSourceBuffer() {
this.source = this.context.createBufferSource();
this.source.buffer = this.audioBuf;
if (this.element && this.source) return; // leave connected, we can (and must) re-use it
if (this.element) {
this.source = this.context.createMediaElementSource(this.element);
} else {
this.source = this.context.createBufferSource();
this.source.buffer = this.audioBuf;
}
this.source.addEventListener("ended", this.onPlaybackEnd);
this.source.connect(this.context.destination);
}
@ -241,7 +282,11 @@ export class Playback extends EventEmitter implements IDestroyable {
// when it comes time to the user hitting play. After a couple jumps, the user
// will have desynced the clock enough to be about 10-15 seconds off, while this
// keeps it as close to perfect as humans can perceive.
this.source.start(now, timeSeconds);
if (this.element) {
this.element.currentTime = timeSeconds;
} else {
(this.source as AudioBufferSourceNode).start(now, timeSeconds);
}
// Dev note: it's critical that the code gap between `this.source.start()` and
// `this.pause()` is as small as possible: we do not want to delay *anything*

View File

@ -103,8 +103,8 @@ export class PlaybackClock implements IDestroyable {
* @param {MatrixEvent} event The event to use for placeholders.
*/
public populatePlaceholdersFrom(event: MatrixEvent) {
const durationSeconds = Number(event.getContent()['info']?.['duration']);
if (Number.isFinite(durationSeconds)) this.placeholderDuration = durationSeconds;
const durationMs = Number(event.getContent()['info']?.['duration']);
if (Number.isFinite(durationMs)) this.placeholderDuration = durationMs / 1000;
}
/**
@ -132,6 +132,10 @@ export class PlaybackClock implements IDestroyable {
public flagStop() {
this.stopped = true;
// Reset the clock time now so that the update going out will trigger components
// to check their seek/position information (alongside the clock).
this.clipStart = this.context.currentTime;
}
public syncTo(contextTime: number, clipTime: number) {

View File

@ -0,0 +1,54 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DEFAULT_WAVEFORM, Playback } from "./Playback";
import { ManagedPlayback } from "./ManagedPlayback";
/**
* Handles management of playback instances to ensure certain functionality, like
* one playback operating at any one time.
*/
export class PlaybackManager {
private static internalInstance: PlaybackManager;
private instances: ManagedPlayback[] = [];
public static get instance(): PlaybackManager {
if (!PlaybackManager.internalInstance) {
PlaybackManager.internalInstance = new PlaybackManager();
}
return PlaybackManager.internalInstance;
}
/**
* Stops all other playback instances. If no playback is provided, all instances
* are stopped.
* @param playback Optional. The playback to leave untouched.
*/
public playOnly(playback?: Playback) {
this.instances.filter(p => p !== playback).forEach(p => p.stop());
}
public destroyPlaybackInstance(playback: ManagedPlayback) {
this.instances = this.instances.filter(p => p !== playback);
}
public createPlaybackInstance(buf: ArrayBuffer, waveform = DEFAULT_WAVEFORM): Playback {
const instance = new ManagedPlayback(this, buf, waveform);
this.instances.push(instance);
return instance;
}
}

View File

@ -333,12 +333,17 @@ export class VoiceRecording extends EventEmitter implements IDestroyable {
if (this.lastUpload) return this.lastUpload;
this.emit(RecordingState.Uploading);
const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
type: this.contentType,
}));
this.lastUpload = { mxc, encrypted };
this.emit(RecordingState.Uploaded);
try {
this.emit(RecordingState.Uploading);
const { url: mxc, file: encrypted } = await uploadFile(this.client, inRoomId, new Blob([this.audioBuffer], {
type: this.contentType,
}));
this.lastUpload = { mxc, encrypted };
this.emit(RecordingState.Uploaded);
} catch (e) {
this.emit(RecordingState.Ended);
throw e;
}
return this.lastUpload;
}
}

View File

@ -25,7 +25,6 @@ import { PillCompletion } from './Components';
import { ICompletion, ISelectionRange } from './Autocompleter';
import { uniq, sortBy } from 'lodash';
import SettingsStore from "../settings/SettingsStore";
import { shortcodeToUnicode } from '../HtmlUtils';
import { EMOJI, IEmoji } from '../emoji';
import EMOTICON_REGEX from 'emojibase-regex/emoticon';
@ -36,20 +35,18 @@ const LIMIT = 20;
// anchored to only match from the start of parts otherwise it'll show emoji suggestions whilst typing matrix IDs
const EMOJI_REGEX = new RegExp('(' + EMOTICON_REGEX.source + '|(?:^|\\s):[+-\\w]*:?)$', 'g');
interface IEmojiShort {
interface ISortedEmoji {
emoji: IEmoji;
shortname: string;
_orderBy: number;
}
const EMOJI_SHORTNAMES: IEmojiShort[] = EMOJI.sort((a, b) => {
const SORTED_EMOJI: ISortedEmoji[] = EMOJI.sort((a, b) => {
if (a.group === b.group) {
return a.order - b.order;
}
return a.group - b.group;
}).map((emoji, index) => ({
emoji,
shortname: `:${emoji.shortcodes[0]}:`,
// Include the index so that we can preserve the original order
_orderBy: index,
}));
@ -64,20 +61,18 @@ function score(query, space) {
}
export default class EmojiProvider extends AutocompleteProvider {
matcher: QueryMatcher<IEmojiShort>;
nameMatcher: QueryMatcher<IEmojiShort>;
matcher: QueryMatcher<ISortedEmoji>;
nameMatcher: QueryMatcher<ISortedEmoji>;
constructor() {
super(EMOJI_REGEX);
this.matcher = new QueryMatcher<IEmojiShort>(EMOJI_SHORTNAMES, {
keys: ['emoji.emoticon', 'shortname'],
funcs: [
(o) => o.emoji.shortcodes.length > 1 ? o.emoji.shortcodes.slice(1).map(s => `:${s}:`).join(" ") : "", // aliases
],
this.matcher = new QueryMatcher<ISortedEmoji>(SORTED_EMOJI, {
keys: ['emoji.emoticon'],
funcs: [o => o.emoji.shortcodes.map(s => `:${s}:`)],
// For matching against ascii equivalents
shouldMatchWordsOnly: false,
});
this.nameMatcher = new QueryMatcher(EMOJI_SHORTNAMES, {
this.nameMatcher = new QueryMatcher(SORTED_EMOJI, {
keys: ['emoji.annotation'],
// For removing punctuation
shouldMatchWordsOnly: true,
@ -105,34 +100,33 @@ export default class EmojiProvider extends AutocompleteProvider {
const sorters = [];
// make sure that emoticons come first
sorters.push((c) => score(matchedString, c.emoji.emoticon || ""));
sorters.push(c => score(matchedString, c.emoji.emoticon || ""));
// then sort by score (Infinity if matchedString not in shortname)
sorters.push((c) => score(matchedString, c.shortname));
// then sort by score (Infinity if matchedString not in shortcode)
sorters.push(c => score(matchedString, c.emoji.shortcodes[0]));
// then sort by max score of all shortcodes, trim off the `:`
sorters.push((c) => Math.min(...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s))));
// If the matchedString is not empty, sort by length of shortname. Example:
sorters.push(c => Math.min(
...c.emoji.shortcodes.map(s => score(matchedString.substring(1), s)),
));
// If the matchedString is not empty, sort by length of shortcode. Example:
// matchedString = ":bookmark"
// completions = [":bookmark:", ":bookmark_tabs:", ...]
if (matchedString.length > 1) {
sorters.push((c) => c.shortname.length);
sorters.push(c => c.emoji.shortcodes[0].length);
}
// Finally, sort by original ordering
sorters.push((c) => c._orderBy);
sorters.push(c => c._orderBy);
completions = sortBy(uniq(completions), sorters);
completions = completions.map(({ shortname }) => {
const unicode = shortcodeToUnicode(shortname);
return {
completion: unicode,
component: (
<PillCompletion title={shortname} aria-label={unicode}>
<span>{ unicode }</span>
</PillCompletion>
),
range,
};
}).slice(0, LIMIT);
completions = completions.map(c => ({
completion: c.emoji.unicode,
component: (
<PillCompletion title={`:${c.emoji.shortcodes[0]}:`} aria-label={c.emoji.unicode}>
<span>{ c.emoji.unicode }</span>
</PillCompletion>
),
range,
})).slice(0, LIMIT);
}
return completions;
}

View File

@ -109,7 +109,7 @@ export default class UserProvider extends AutocompleteProvider {
limit = -1,
): Promise<ICompletion[]> {
// lazy-load user list into matcher
if (!this.users) this._makeUsers();
if (!this.users) this.makeUsers();
let completions = [];
const { command, range } = this.getCurrentCommand(rawQuery, selection, force);
@ -147,7 +147,7 @@ export default class UserProvider extends AutocompleteProvider {
return _t('Users');
}
_makeUsers() {
private makeUsers() {
const events = this.room.getLiveTimeline().getEvents();
const lastSpoken = {};

View File

@ -0,0 +1,153 @@
/*
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { EventType } from "matrix-js-sdk/src/@types/event";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { CallEvent, CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import CallHandler, { CallHandlerEvent } from '../../CallHandler';
import { EventEmitter } from 'events';
import { MatrixClientPeg } from "../../MatrixClientPeg";
import defaultDispatcher from "../../dispatcher/dispatcher";
export enum CallEventGrouperEvent {
StateChanged = "state_changed",
SilencedChanged = "silenced_changed",
}
const SUPPORTED_STATES = [
CallState.Connected,
CallState.Connecting,
CallState.Ringing,
];
export enum CustomCallState {
Missed = "missed",
}
export default class CallEventGrouper extends EventEmitter {
private events: Set<MatrixEvent> = new Set<MatrixEvent>();
private call: MatrixCall;
public state: CallState | CustomCallState;
constructor() {
super();
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.setCall);
CallHandler.sharedInstance().addListener(CallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged);
}
private get invite(): MatrixEvent {
return [...this.events].find((event) => event.getType() === EventType.CallInvite);
}
private get hangup(): MatrixEvent {
return [...this.events].find((event) => event.getType() === EventType.CallHangup);
}
private get reject(): MatrixEvent {
return [...this.events].find((event) => event.getType() === EventType.CallReject);
}
public get isVoice(): boolean {
const invite = this.invite;
if (!invite) return;
// FIXME: Find a better way to determine this from the event?
if (invite.getContent()?.offer?.sdp?.indexOf('m=video') !== -1) return false;
return true;
}
public get hangupReason(): string | null {
return this.hangup?.getContent()?.reason;
}
public get rejectParty(): string {
return this.reject?.getSender();
}
public get gotRejected(): boolean {
return Boolean(this.reject);
}
/**
* Returns true if there are only events from the other side - we missed the call
*/
private get callWasMissed(): boolean {
return ![...this.events].some((event) => event.sender?.userId === MatrixClientPeg.get().getUserId());
}
private get callId(): string {
return [...this.events][0].getContent().call_id;
}
private onSilencedCallsChanged = () => {
const newState = CallHandler.sharedInstance().isCallSilenced(this.callId);
this.emit(CallEventGrouperEvent.SilencedChanged, newState);
};
public answerCall = () => {
this.call?.answer();
};
public rejectCall = () => {
this.call?.reject();
};
public callBack = () => {
defaultDispatcher.dispatch({
action: 'place_call',
type: this.isVoice ? CallType.Voice : CallType.Video,
room_id: [...this.events][0]?.getRoomId(),
});
};
public toggleSilenced = () => {
const silenced = CallHandler.sharedInstance().isCallSilenced(this.callId);
silenced ?
CallHandler.sharedInstance().unSilenceCall(this.callId) :
CallHandler.sharedInstance().silenceCall(this.callId);
};
private setCallListeners() {
if (!this.call) return;
this.call.addListener(CallEvent.State, this.setState);
}
private setState = () => {
if (SUPPORTED_STATES.includes(this.call?.state)) {
this.state = this.call.state;
} else {
if (this.callWasMissed) this.state = CustomCallState.Missed;
else if (this.reject) this.state = CallState.Ended;
else if (this.hangup) this.state = CallState.Ended;
else if (this.invite && this.call) this.state = CallState.Connecting;
}
this.emit(CallEventGrouperEvent.StateChanged, this.state);
};
private setCall = () => {
if (this.call) return;
this.call = CallHandler.sharedInstance().getCallById(this.callId);
this.setCallListeners();
this.setState();
};
public add(event: MatrixEvent) {
this.events.add(event);
this.setCall();
}
}

View File

@ -56,7 +56,7 @@ class CustomRoomTagPanel extends React.Component {
return (<div className={classes}>
<div className="mx_CustomRoomTagPanel_divider" />
<AutoHideScrollbar className="mx_CustomRoomTagPanel_scroller">
{tags}
{ tags }
</AutoHideScrollbar>
</div>);
}
@ -84,7 +84,7 @@ class CustomRoomTagTile extends React.Component {
"mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
});
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
badgeElement = (<div className={badgeClasses}>{ FormattingUtils.formatCount(badgeNotifState.count) }</div>);
}
return (

View File

@ -120,16 +120,15 @@ export default class EmbeddedPage extends React.PureComponent {
const content = <div className={`${className}_body`}
dangerouslySetInnerHTML={{ __html: this.state.page }}
>
</div>;
/>;
if (this.props.scrollbar) {
return <AutoHideScrollbar className={classes}>
{content}
{ content }
</AutoHideScrollbar>;
} else {
return <div className={classes}>
{content}
{ content }
</div>;
}
}

View File

@ -36,6 +36,7 @@ import ResizeNotifier from '../../utils/ResizeNotifier';
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from '../views/rooms/EventTile';
import { Layout } from "../../settings/Layout";
interface IProps {
roomId: string;
@ -241,8 +242,8 @@ class FilePanel extends React.Component<IProps, IState> {
// wrap a TimelinePanel with the jump-to-event bits turned off.
const emptyState = (<div className="mx_RightPanel_empty mx_FilePanel_empty">
<h2>{_t('No files visible in this room')}</h2>
<p>{_t('Attach files from chat or just drag and drop them anywhere in a room.')}</p>
<h2>{ _t('No files visible in this room') }</h2>
<p>{ _t('Attach files from chat or just drag and drop them anywhere in a room.') }</p>
</div>);
const isRoomEncrypted = this.noRoom ? false : MatrixClientPeg.get().isRoomEncrypted(this.props.roomId);
@ -262,11 +263,12 @@ class FilePanel extends React.Component<IProps, IState> {
manageReadReceipts={false}
manageReadMarkers={false}
timelineSet={this.state.timelineSet}
showUrlPreview = {false}
showUrlPreview={false}
onPaginationRequest={this.onPaginationRequest}
tileShape={TileShape.FileGrid}
resizeNotifier={this.props.resizeNotifier}
empty={emptyState}
layout={Layout.Group}
/>
</BaseCard>
);

View File

@ -28,8 +28,8 @@ export default class GenericErrorPage extends React.PureComponent {
render() {
return <div className='mx_GenericErrorPage'>
<div className='mx_GenericErrorPage_box'>
<h1>{this.props.title}</h1>
<p>{this.props.message}</p>
<h1>{ this.props.title }</h1>
<p>{ this.props.message }</p>
</div>
</div>;
}

View File

@ -222,7 +222,7 @@ class FeaturedRoom extends React.Component {
let roomNameNode = null;
if (permalink) {
roomNameNode = <a href={permalink} onClick={this.onClick} >{ roomName }</a>;
roomNameNode = <a href={permalink} onClick={this.onClick}>{ roomName }</a>;
} else {
roomNameNode = <span>{ roomName }</span>;
}
@ -819,12 +819,12 @@ export default class GroupView extends React.Component {
let hostingSignup = null;
if (hostingSignupLink && this.state.isUserPrivileged) {
hostingSignup = <div className="mx_GroupView_hostingSignup">
{_t(
{ _t(
"Want more than a community? <a>Get your own server</a>", {},
{
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{sub}</a>,
a: sub => <a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">{ sub }</a>,
},
)}
) }
<a href={hostingSignupLink} target="_blank" rel="noreferrer noopener">
<img src={require("../../../res/img/external-link.svg")} width="11" height="10" alt='' />
</a>
@ -1185,10 +1185,13 @@ export default class GroupView extends React.Component {
avatarImage = <Spinner />;
} else {
const GroupAvatar = sdk.getComponent('avatars.GroupAvatar');
avatarImage = <GroupAvatar groupId={this.props.groupId}
avatarImage = <GroupAvatar
groupId={this.props.groupId}
groupName={this.state.profileForm.name}
groupAvatarUrl={this.state.profileForm.avatar_url}
width={28} height={28} resizeMethod='crop'
width={28}
height={28}
resizeMethod='crop'
/>;
}
@ -1199,9 +1202,12 @@ export default class GroupView extends React.Component {
</label>
<div className="mx_GroupView_avatarPicker_edit">
<label htmlFor="avatarInput" className="mx_GroupView_avatarPicker_label">
<img src={require("../../../res/img/camera.svg")}
alt={_t("Upload avatar")} title={_t("Upload avatar")}
width="17" height="15" />
<img
src={require("../../../res/img/camera.svg")}
alt={_t("Upload avatar")}
title={_t("Upload avatar")}
width="17"
height="15" />
</label>
<input id="avatarInput" className="mx_GroupView_uploadInput" type="file" onChange={this._onAvatarSelected} />
</div>
@ -1238,7 +1244,8 @@ export default class GroupView extends React.Component {
groupAvatarUrl={groupAvatarUrl}
groupName={groupName}
onClick={onGroupHeaderItemClick}
width={28} height={28}
width={28}
height={28}
/>;
if (summary.profile && summary.profile.name) {
nameNode = <div onClick={onGroupHeaderItemClick}>
@ -1269,28 +1276,32 @@ export default class GroupView extends React.Component {
key="_cancelButton"
onClick={this._onCancelClick}
>
<img src={require("../../../res/img/cancel.svg")} className="mx_filterFlipColor"
width="18" height="18" alt={_t("Cancel")} />
<img
src={require("../../../res/img/cancel.svg")}
className="mx_filterFlipColor"
width="18"
height="18"
alt={_t("Cancel")} />
</AccessibleButton>,
);
} else {
if (summary.user && summary.user.membership === 'join') {
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_editButton"
<AccessibleButton
className="mx_GroupHeader_button mx_GroupHeader_editButton"
key="_editButton"
onClick={this._onEditClick}
title={_t("Community Settings")}
>
</AccessibleButton>,
/>,
);
}
rightButtons.push(
<AccessibleButton className="mx_GroupHeader_button mx_GroupHeader_shareButton"
<AccessibleButton
className="mx_GroupHeader_button mx_GroupHeader_shareButton"
key="_shareButton"
onClick={this._onShareClick}
title={_t('Share Community')}
>
</AccessibleButton>,
/>,
);
}

View File

@ -429,7 +429,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
onSelectRoom={this.selectRoom}
/>
{dialPadButton}
{ dialPadButton }
<AccessibleTooltipButton
className={classNames("mx_LeftPanel_exploreButton", {
@ -448,7 +448,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
leftLeftPanel = (
<div className="mx_LeftPanel_GroupFilterPanelContainer">
<GroupFilterPanel />
{SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
{ SettingsStore.getValue("feature_custom_tags") ? <CustomRoomTagPanel /> : null }
</div>
);
}
@ -476,11 +476,11 @@ export default class LeftPanel extends React.Component<IProps, IState> {
return (
<div className={containerClasses} ref={this.ref}>
{leftLeftPanel}
{ leftLeftPanel }
<aside className="mx_LeftPanel_roomListContainer">
{this.renderHeader()}
{this.renderSearchDialExplore()}
{this.renderBreadcrumbs()}
{ this.renderHeader() }
{ this.renderSearchDialExplore() }
{ this.renderBreadcrumbs() }
<RoomListNumResults onVisibilityChange={this.refreshStickyHeaders} />
<div className="mx_LeftPanel_roomListWrapper">
<div
@ -490,7 +490,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
// overflow:scroll;, so force it out of tab order.
tabIndex={-1}
>
{roomList}
{ roomList }
</div>
</div>
{ !this.props.isMinimized && <LeftPanelWidget /> }

View File

@ -125,15 +125,15 @@ const LeftPanelWidget: React.FC = () => {
<span>{ WidgetUtils.getWidgetName(app) }</span>
</AccessibleButton>
{/* Code for the maximise button for once we have full screen widgets */}
{/*<AccessibleTooltipButton
{ /* Code for the maximise button for once we have full screen widgets */ }
{ /*<AccessibleTooltipButton
tabIndex={tabIndex}
onClick={() => {
}}
className="mx_LeftPanelWidget_maximizeButton"
tooltipClassName="mx_LeftPanelWidget_maximizeButtonTooltip"
title={_t("Maximize")}
/>*/}
/>*/ }
</div>
</div>

View File

@ -17,8 +17,8 @@ limitations under the License.
*/
import * as React from 'react';
import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client';
import { MatrixEvent } from 'matrix-js-sdk/src/models/event';
import { Key } from '../../Keyboard';
import PageTypes from '../../PageTypes';
@ -79,6 +79,8 @@ function canElementReceiveInput(el) {
interface IProps {
matrixClient: MatrixClient;
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: (credentials: IMatrixClientCreds) => Promise<MatrixClient>;
hideToSRUsers: boolean;
resizeNotifier: ResizeNotifier;
@ -140,18 +142,6 @@ interface IState {
class LoggedInView extends React.Component<IProps, IState> {
static displayName = 'LoggedInView';
static propTypes = {
matrixClient: PropTypes.instanceOf(MatrixClient).isRequired,
page_type: PropTypes.string.isRequired,
onRoomCreated: PropTypes.func,
// Called with the credentials of a registered user (if they were a ROU that
// transitioned to PWLU)
onRegistered: PropTypes.func,
// and lots and lots of other stuff.
};
protected readonly _matrixClient: MatrixClient;
protected readonly _roomView: React.RefObject<any>;
protected readonly _resizeContainer: React.RefObject<ResizeHandle>;
@ -181,10 +171,10 @@ class LoggedInView extends React.Component<IProps, IState> {
}
componentDidMount() {
document.addEventListener('keydown', this._onNativeKeyDown, false);
document.addEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.sharedInstance().addListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._updateServerNoticeEvents();
this.updateServerNoticeEvents();
this._matrixClient.on("accountData", this.onAccountData);
this._matrixClient.on("sync", this.onSync);
@ -200,13 +190,13 @@ class LoggedInView extends React.Component<IProps, IState> {
"useCompactLayout", null, this.onCompactLayoutChanged,
);
this.resizer = this._createResizer();
this.resizer = this.createResizer();
this.resizer.attach();
this._loadResizerPreferences();
this.loadResizerPreferences();
}
componentWillUnmount() {
document.removeEventListener('keydown', this._onNativeKeyDown, false);
document.removeEventListener('keydown', this.onNativeKeyDown, false);
CallHandler.sharedInstance().removeListener(CallHandlerEvent.CallsChanged, this.onCallsChanged);
this._matrixClient.removeListener("accountData", this.onAccountData);
this._matrixClient.removeListener("sync", this.onSync);
@ -221,37 +211,37 @@ class LoggedInView extends React.Component<IProps, IState> {
});
};
canResetTimelineInRoom = (roomId) => {
public canResetTimelineInRoom = (roomId: string) => {
if (!this._roomView.current) {
return true;
}
return this._roomView.current.canResetTimeline();
};
_createResizer() {
let size;
let collapsed;
private createResizer() {
let panelSize;
let panelCollapsed;
const collapseConfig: ICollapseConfig = {
// TODO decrease this once Spaces launches as it'll no longer need to include the 56px Community Panel
toggleSize: 206 - 50,
onCollapsed: (_collapsed) => {
collapsed = _collapsed;
if (_collapsed) {
onCollapsed: (collapsed) => {
panelCollapsed = collapsed;
if (collapsed) {
dis.dispatch({ action: "hide_left_panel" });
window.localStorage.setItem("mx_lhs_size", '0');
} else {
dis.dispatch({ action: "show_left_panel" });
}
},
onResized: (_size) => {
size = _size;
onResized: (size) => {
panelSize = size;
this.props.resizeNotifier.notifyLeftHandleResized();
},
onResizeStart: () => {
this.props.resizeNotifier.startResizing();
},
onResizeStop: () => {
if (!collapsed) window.localStorage.setItem("mx_lhs_size", '' + size);
if (!panelCollapsed) window.localStorage.setItem("mx_lhs_size", '' + panelSize);
this.props.resizeNotifier.stopResizing();
},
isItemCollapsed: domNode => {
@ -267,7 +257,7 @@ class LoggedInView extends React.Component<IProps, IState> {
return resizer;
}
_loadResizerPreferences() {
private loadResizerPreferences() {
let lhsSize = parseInt(window.localStorage.getItem("mx_lhs_size"), 10);
if (isNaN(lhsSize)) {
lhsSize = 350;
@ -275,7 +265,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this.resizer.forHandleAt(0).resize(lhsSize);
}
onAccountData = (event) => {
private onAccountData = (event: MatrixEvent) => {
if (event.getType() === "m.ignored_user_list") {
dis.dispatch({ action: "ignore_state_changed" });
}
@ -307,16 +297,16 @@ class LoggedInView extends React.Component<IProps, IState> {
}
if (oldSyncState === 'PREPARED' && syncState === 'SYNCING') {
this._updateServerNoticeEvents();
this.updateServerNoticeEvents();
} else {
this._calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
this.calculateServerLimitToast(this.state.syncErrorData, this.state.usageLimitEventContent);
}
};
onRoomStateEvents = (ev, state) => {
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (serverNoticeList && serverNoticeList.some(r => r.roomId === ev.getRoomId())) {
this._updateServerNoticeEvents();
this.updateServerNoticeEvents();
}
};
@ -326,7 +316,7 @@ class LoggedInView extends React.Component<IProps, IState> {
});
};
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
private calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";
if (error) {
usageLimitEventContent = syncError.error.data;
@ -346,7 +336,7 @@ class LoggedInView extends React.Component<IProps, IState> {
}
}
_updateServerNoticeEvents = async () => {
private updateServerNoticeEvents = async () => {
const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice];
if (!serverNoticeList) return [];
@ -378,7 +368,7 @@ class LoggedInView extends React.Component<IProps, IState> {
);
});
const usageLimitEventContent = usageLimitEvent && usageLimitEvent.getContent();
this._calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
this.calculateServerLimitToast(this.state.syncErrorData, usageLimitEventContent);
this.setState({
usageLimitEventContent,
usageLimitEventTs: pinnedEventTs,
@ -387,7 +377,7 @@ class LoggedInView extends React.Component<IProps, IState> {
});
};
_onPaste = (ev) => {
private onPaste = (ev) => {
let canReceiveInput = false;
let element = ev.target;
// test for all parents because the target can be a child of a contenteditable element
@ -425,22 +415,22 @@ class LoggedInView extends React.Component<IProps, IState> {
We also listen with a native listener on the document to get keydown events when no element is focused.
Bubbling is irrelevant here as the target is the body element.
*/
_onReactKeyDown = (ev) => {
private onReactKeyDown = (ev) => {
// events caught while bubbling up on the root element
// of this component, so something must be focused.
this._onKeyDown(ev);
this.onKeyDown(ev);
};
_onNativeKeyDown = (ev) => {
private onNativeKeyDown = (ev) => {
// only pass this if there is no focused element.
// if there is, _onKeyDown will be called by the
// if there is, onKeyDown will be called by the
// react keydown handler that respects the react bubbling order.
if (ev.target === document.body) {
this._onKeyDown(ev);
this.onKeyDown(ev);
}
};
_onKeyDown = (ev) => {
private onKeyDown = (ev) => {
let handled = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
@ -450,7 +440,7 @@ class LoggedInView extends React.Component<IProps, IState> {
case RoomAction.JumpToFirstMessage:
case RoomAction.JumpToLatestMessage:
// pass the event down to the scroll panel
this._onScrollKeyPressed(ev);
this.onScrollKeyPressed(ev);
handled = true;
break;
case RoomAction.FocusSearch:
@ -565,7 +555,7 @@ class LoggedInView extends React.Component<IProps, IState> {
* dispatch a page-up/page-down/etc to the appropriate component
* @param {Object} ev The key event
*/
_onScrollKeyPressed = (ev) => {
private onScrollKeyPressed = (ev) => {
if (this._roomView.current) {
this._roomView.current.handleScrollKey(ev);
}
@ -625,8 +615,8 @@ class LoggedInView extends React.Component<IProps, IState> {
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<div
onPaste={this._onPaste}
onKeyDown={this._onReactKeyDown}
onPaste={this.onPaste}
onKeyDown={this.onReactKeyDown}
className='mx_MatrixChat_wrapper'
aria-hidden={this.props.hideToSRUsers}
>
@ -644,7 +634,7 @@ class LoggedInView extends React.Component<IProps, IState> {
<CallContainer />
<NonUrgentToastContainer />
<HostSignupContainer />
{audioFeedArraysForCalls}
{ audioFeedArraysForCalls }
</MatrixClientContext.Provider>
);
}

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