Merge remote-tracking branch 'origin' into posthog-analytics
149
CHANGELOG.md
@ -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)
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
150
res/css/views/dialogs/_ManageRestrictedJoinRuleDialog.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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;
|
||||
|
162
res/css/views/messages/_CallEvent.scss
Normal 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');
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ limitations under the License.
|
||||
height: 24px;
|
||||
vertical-align: middle;
|
||||
margin-left: 4px;
|
||||
margin-right: 4px;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
|
335
res/css/views/rooms/_EventBubbleTile.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -26,6 +26,7 @@ $left-gutter: 64px;
|
||||
|
||||
> .mx_EventTile_avatar {
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.mx_MessageTimestamp {
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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 */
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
52
res/css/views/voip/_CallViewSidebar.scss
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
3
res/img/element-icons/warning.svg
Normal 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 |
@ -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 |
3
res/img/voip/missed-video.svg
Normal 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 |
4
res/img/voip/missed-voice.svg
Normal 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 |
18
res/img/voip/screensharing-off.svg
Normal 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 |
18
res/img/voip/screensharing-on.svg
Normal 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 |
20
res/img/voip/sidebar-off.svg
Normal 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 |
19
res/img/voip/sidebar-on.svg
Normal 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 |
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
22
src/@types/global.d.ts
vendored
@ -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
@ -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;
|
||||
}
|
@ -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) =>
|
||||
|
@ -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 it’s 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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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"),
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"),
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
|
@ -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 = {
|
||||
|
@ -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)) {
|
||||
|
@ -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>;
|
||||
});
|
||||
|
||||
|
@ -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>;
|
||||
};
|
||||
|
||||
|
@ -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}
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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("We’ll generate a Security Key for you to store somewhere safe, like a password manager or a safe.")}</div>
|
||||
<div>{ _t("We’ll 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 it’s used to safeguard your data. " +
|
||||
"To be secure, you shouldn’t 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 it’s 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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
@ -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}>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
|
37
src/audio/ManagedPlayback.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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*
|
@ -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) {
|
54
src/audio/PlaybackManager.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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 = {};
|
||||
|
||||
|
153
src/components/structures/CallEventGrouper.ts
Normal 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();
|
||||
}
|
||||
}
|
@ -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 (
|
||||
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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>,
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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 /> }
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|