Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into t3chguy/socials

This commit is contained in:
Michael Telatynski 2020-11-24 11:30:51 +00:00
commit b1ca1eb3f5
53 changed files with 1584 additions and 543 deletions

View file

@ -1,3 +1,73 @@
Changes in [3.9.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0) (2020-11-23)
===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.9.0-rc.1...v3.9.0)
* Upgrade JS SDK to 9.2.0
* [Release] Fix encrypted video playback in Chrome-based browsers
[\#5431](https://github.com/matrix-org/matrix-react-sdk/pull/5431)
Changes in [3.9.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.9.0-rc.1) (2020-11-18)
=============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0...v3.9.0-rc.1)
* Upgrade JS SDK to 9.2.0-rc.1
* Translations update from Weblate
[\#5429](https://github.com/matrix-org/matrix-react-sdk/pull/5429)
* Fix message search summary text
[\#5428](https://github.com/matrix-org/matrix-react-sdk/pull/5428)
* Shrink new room intro top margin to half for encryption bubble tile
[\#5427](https://github.com/matrix-org/matrix-react-sdk/pull/5427)
* Small delight tweaks to improve rough corners in the app
[\#5418](https://github.com/matrix-org/matrix-react-sdk/pull/5418)
* Fix DM logic to always pick a more reliable DM room
[\#5424](https://github.com/matrix-org/matrix-react-sdk/pull/5424)
* Update styling of the Analytics toast
[\#5408](https://github.com/matrix-org/matrix-react-sdk/pull/5408)
* Fix vertical centering of the Homepage and button layout
[\#5420](https://github.com/matrix-org/matrix-react-sdk/pull/5420)
* Fix BaseAvatar sometimes messing up and duplicating the url
[\#5422](https://github.com/matrix-org/matrix-react-sdk/pull/5422)
* Disable buttons when required by MSC2790
[\#5412](https://github.com/matrix-org/matrix-react-sdk/pull/5412)
* Fix drag drop file to upload for Safari
[\#5414](https://github.com/matrix-org/matrix-react-sdk/pull/5414)
* Fix poorly i18n'd string
[\#5416](https://github.com/matrix-org/matrix-react-sdk/pull/5416)
* Fix the feedback not closing without feedback/countly
[\#5417](https://github.com/matrix-org/matrix-react-sdk/pull/5417)
* Fix New Room Intro invite to this room button
[\#5419](https://github.com/matrix-org/matrix-react-sdk/pull/5419)
* Change how we expose Role in User Info and hide in DMs
[\#5413](https://github.com/matrix-org/matrix-react-sdk/pull/5413)
* Disallow sending of empty messages
[\#5390](https://github.com/matrix-org/matrix-react-sdk/pull/5390)
* hide some validation tooltips if fields are valid.
[\#5403](https://github.com/matrix-org/matrix-react-sdk/pull/5403)
* Improvements around new room empty space interactions
[\#5398](https://github.com/matrix-org/matrix-react-sdk/pull/5398)
* Implement call hold
[\#5366](https://github.com/matrix-org/matrix-react-sdk/pull/5366)
* Fix Skeleton UI showing up when not intended.
[\#5407](https://github.com/matrix-org/matrix-react-sdk/pull/5407)
* Close context menu when user clicks the Home button
[\#5406](https://github.com/matrix-org/matrix-react-sdk/pull/5406)
* Skip e2ee warn logout prompt if user has no megolm sessions to lose
[\#5410](https://github.com/matrix-org/matrix-react-sdk/pull/5410)
* Allow country names to be translated
[\#5405](https://github.com/matrix-org/matrix-react-sdk/pull/5405)
* Support thirdparty lookup for phone numbers
[\#5396](https://github.com/matrix-org/matrix-react-sdk/pull/5396)
* Change "Password" to "New Password"
[\#5371](https://github.com/matrix-org/matrix-react-sdk/pull/5371)
* Add customisation point for dehydration key
[\#5397](https://github.com/matrix-org/matrix-react-sdk/pull/5397)
* Rebrand Riot -> Element in the permalink classes
[\#5386](https://github.com/matrix-org/matrix-react-sdk/pull/5386)
* Invite / Create DM UX tweaks
[\#5387](https://github.com/matrix-org/matrix-react-sdk/pull/5387)
* Tweaks to toasts and post-registration landing
[\#5383](https://github.com/matrix-org/matrix-react-sdk/pull/5383)
Changes in [3.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0) (2020-11-09) Changes in [3.8.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.8.0) (2020-11-09)
=================================================================================================== ===================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0-rc.1...v3.8.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.8.0-rc.1...v3.8.0)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.8.0", "version": "3.9.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -79,7 +79,7 @@
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"matrix-widget-api": "^0.1.0-beta.8", "matrix-widget-api": "^0.1.0-beta.9",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"pako": "^1.0.11", "pako": "^1.0.11",
"parse5": "^5.1.1", "parse5": "^5.1.1",

View file

@ -60,6 +60,10 @@ pre, code {
color: $accent-color; color: $accent-color;
} }
.text-muted {
color: $muted-fg-color;
}
b { b {
// On Firefox, the default weight for `<b>` is `bolder` which results in no bold // On Firefox, the default weight for `<b>` is `bolder` which results in no bold
// effect since we only have specific weights of our fonts available. // effect since we only have specific weights of our fonts available.
@ -364,6 +368,11 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
.mx_Dialog_buttons { .mx_Dialog_buttons {
margin-top: 20px; margin-top: 20px;
text-align: right; text-align: right;
.mx_Dialog_buttons_additive {
// The consumer is responsible for positioning their elements.
float: left;
}
} }
/* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied /* XXX: Our button style are a mess: buttons that happen to appear in dialogs get special styles applied

View file

@ -92,6 +92,7 @@
@import "./views/dialogs/_TermsDialog.scss"; @import "./views/dialogs/_TermsDialog.scss";
@import "./views/dialogs/_UploadConfirmDialog.scss"; @import "./views/dialogs/_UploadConfirmDialog.scss";
@import "./views/dialogs/_UserSettingsDialog.scss"; @import "./views/dialogs/_UserSettingsDialog.scss";
@import "./views/dialogs/_WidgetCapabilitiesPromptDialog.scss";
@import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss"; @import "./views/dialogs/_WidgetOpenIDPermissionsDialog.scss";
@import "./views/dialogs/security/_AccessSecretStorageDialog.scss"; @import "./views/dialogs/security/_AccessSecretStorageDialog.scss";
@import "./views/dialogs/security/_CreateCrossSigningDialog.scss"; @import "./views/dialogs/security/_CreateCrossSigningDialog.scss";

View file

@ -19,57 +19,6 @@ limitations under the License.
min-height: 50px; min-height: 50px;
} }
/* position the indicator in the same place horizontally as .mx_EventTile_avatar. */
.mx_RoomStatusBar_indicator {
padding-left: 17px;
padding-right: 12px;
margin-left: -73px;
margin-top: 15px;
float: left;
width: 24px;
text-align: center;
}
.mx_RoomStatusBar_callBar {
height: 50px;
line-height: $font-50px;
}
.mx_RoomStatusBar_placeholderIndicator span {
color: $primary-fg-color;
opacity: 0.5;
position: relative;
top: -4px;
/*
animation-duration: 1s;
animation-name: bounce;
animation-direction: alternate;
animation-iteration-count: infinite;
*/
}
.mx_RoomStatusBar_placeholderIndicator span:nth-child(1) {
animation-delay: 0.3s;
}
.mx_RoomStatusBar_placeholderIndicator span:nth-child(2) {
animation-delay: 0.6s;
}
.mx_RoomStatusBar_placeholderIndicator span:nth-child(3) {
animation-delay: 0.9s;
}
@keyframes bounce {
from {
opacity: 0.5;
top: 0;
}
to {
opacity: 0.2;
top: -3px;
}
}
.mx_RoomStatusBar_typingIndicatorAvatars { .mx_RoomStatusBar_typingIndicatorAvatars {
width: 52px; width: 52px;
margin-top: -1px; margin-top: -1px;
@ -162,11 +111,6 @@ limitations under the License.
margin-top: 10px; margin-top: 10px;
} }
.mx_RoomStatusBar_callBar {
height: 40px;
line-height: $font-40px;
}
.mx_RoomStatusBar_typingBar { .mx_RoomStatusBar_typingBar {
height: 40px; height: 40px;
line-height: $font-40px; line-height: $font-40px;

View file

@ -41,7 +41,7 @@ limitations under the License.
.mx_BaseAvatar_image { .mx_BaseAvatar_image {
object-fit: cover; object-fit: cover;
border-radius: 40px; border-radius: 125px;
vertical-align: top; vertical-align: top;
background-color: $avatar-bg-color; background-color: $avatar-bg-color;
} }

View file

@ -0,0 +1,75 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_WidgetCapabilitiesPromptDialog {
.text-muted {
font-size: $font-12px;
}
.mx_Dialog_content {
margin-bottom: 16px;
}
.mx_WidgetCapabilitiesPromptDialog_cap {
margin-top: 20px;
font-size: $font-15px;
line-height: $font-15px;
.mx_WidgetCapabilitiesPromptDialog_byline {
color: $muted-fg-color;
margin-left: 26px;
font-size: $font-12px;
line-height: $font-12px;
}
}
.mx_Dialog_buttons {
margin-top: 40px; // double normal
}
.mx_SettingsFlag {
line-height: calc($font-14px + 7px + 7px); // 7px top & bottom padding
color: $muted-fg-color;
font-size: $font-12px;
.mx_ToggleSwitch {
display: inline-block;
vertical-align: middle;
margin-right: 8px;
// downsize the switch + ball
width: $font-32px;
height: $font-15px;
&.mx_ToggleSwitch_on > .mx_ToggleSwitch_ball {
left: calc(100% - $font-15px);
}
.mx_ToggleSwitch_ball {
width: $font-15px;
height: $font-15px;
border-radius: $font-15px;
}
}
.mx_SettingsFlag_label {
display: inline-block;
vertical-align: middle;
}
}
}

View file

@ -15,82 +15,37 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_CallView {
border-radius: 10px;
background-color: $input-lighter-bg-color;
padding-left: 8px;
padding-right: 8px;
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
pointer-events: initial;
}
.mx_CallView_large {
padding-bottom: 10px;
.mx_CallView_voice { .mx_CallView_voice {
background-color: $accent-color; height: 360px;
color: $accent-fg-color; }
cursor: pointer; }
padding: 6px;
font-weight: bold;
border-radius: 8px; .mx_CallView_pip {
min-width: 200px; width: 320px;
.mx_CallView_voice {
height: 180px;
}
}
.mx_CallView_voice {
position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
img { background-color: $inverted-bg-color;
margin: 4px;
margin-right: 10px;
}
> div {
display: flex;
flex-direction: column;
// Hacky vertical align
padding-top: 3px;
}
> div > p,
> div > h1 {
padding: 0;
margin: 0;
font-size: $font-13px;
line-height: $font-15px;
}
> div > p {
font-weight: bold;
}
> * {
flex-grow: 0;
flex-shrink: 0;
}
}
.mx_CallView_hangup {
position: absolute;
right: 8px;
bottom: 10px;
height: 35px;
width: 35px;
border-radius: 35px;
background-color: $notice-primary-color;
z-index: 101;
cursor: pointer;
&::before {
content: '';
position: absolute;
height: 20px;
width: 20px;
top: 6.5px;
left: 7.5px;
mask: url('$(res)/img/hangup.svg');
mask-size: contain;
background-size: contain;
background-color: $primary-fg-color;
}
} }
.mx_CallView_video { .mx_CallView_video {
@ -99,3 +54,157 @@ limitations under the License.
z-index: 30; z-index: 30;
} }
.mx_CallView_header {
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: left;
.mx_BaseAvatar {
margin-right: 12px;
}
}
.mx_CallView_header_callType {
font-weight: bold;
vertical-align: middle;
}
.mx_CallView_header_controls {
margin-left: auto;
}
.mx_CallView_header_button {
display: inline-block;
vertical-align: middle;
cursor: pointer;
&::before {
content: '';
display: inline-block;
height: 20px;
width: 20px;
vertical-align: middle;
background-color: $secondary-fg-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.mx_CallView_header_button_fullscreen {
&::before {
mask-image: url('$(res)/img/element-icons/call/fullscreen.svg');
}
}
.mx_CallView_header_button_expand {
&::before {
mask-image: url('$(res)/img/element-icons/call/expand.svg');
}
}
.mx_CallView_header_roomName {
font-weight: bold;
font-size: 12px;
line-height: initial;
}
.mx_CallView_header_callTypeSmall {
font-size: 12px;
color: $secondary-fg-color;
line-height: initial;
}
.mx_CallView_header_phoneIcon {
display: inline-block;
margin-right: 6px;
height: 16px;
width: 16px;
vertical-align: middle;
&::before {
content: '';
display: inline-block;
vertical-align: top;
height: 16px;
width: 16px;
background-color: $warning-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
mask-image: url('$(res)/img/element-icons/call/voice-call.svg');
}
}
.mx_CallView_callControls {
position: absolute;
display: flex;
justify-content: center;
bottom: 5px;
width: 100%;
opacity: 1;
transition: opacity 0.5s;
}
.mx_CallView_callControls_hidden {
opacity: 0.001; // opacity 0 can cause a re-layout
pointer-events: none;
}
.mx_CallView_callControls_button {
cursor: pointer;
margin-left: 8px;
margin-right: 8px;
&::before {
content: '';
display: inline-block;
height: 48px;
width: 48px;
background-repeat: no-repeat;
background-size: contain;
background-position: center;
}
}
.mx_CallView_callControls_button_micOn {
&::before {
background-image: url('$(res)/img/voip/mic-on.svg');
}
}
.mx_CallView_callControls_button_micOff {
&::before {
background-image: url('$(res)/img/voip/mic-off.svg');
}
}
.mx_CallView_callControls_button_vidOn {
&::before {
background-image: url('$(res)/img/voip/vid-on.svg');
}
}
.mx_CallView_callControls_button_vidOff {
&::before {
background-image: url('$(res)/img/voip/vid-off.svg');
}
}
.mx_CallView_callControls_button_hangup {
&::before {
background-image: url('$(res)/img/voip/hangup.svg');
}
}
.mx_CallView_callControls_button_invisible {
visibility: hidden;
pointer-events: none;
position: absolute;
}

View file

@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_VideoFeed video {
width: 100%;
}
.mx_VideoFeed_remote { .mx_VideoFeed_remote {
width: 100%; width: 100%;
background-color: #000; background-color: #000;
@ -28,16 +24,12 @@ limitations under the License.
width: 25%; width: 25%;
height: 25%; height: 25%;
position: absolute; position: absolute;
left: 10px; right: 10px;
bottom: 10px; top: 10px;
z-index: 100; z-index: 100;
border-radius: 4px;
} }
.mx_VideoFeed_local video { .mx_VideoFeed_mirror {
width: auto;
height: 100%;
}
.mx_VideoFeed_mirror video {
transform: scale(-1, 1); transform: scale(-1, 1);
} }

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 19H18C18.55 19 19 18.55 19 18V6C19 5.45 18.55 5 18 5H13C12.45 5 12 4.55 12 4C12 3.45 12.45 3 13 3H19C20.11 3 21 3.9 21 5V19C21 20.1 20.1 21 19 21H5C3.9 21 3 20.1 3 19V13C3 12.45 3.45 12 4 12C4.55 12 5 12.45 5 13V18C5 18.55 5.45 19 6 19ZM10 4C10 4.55 9.55 5 9 5H6.41L15.54 14.13C15.93 14.52 15.93 15.15 15.54 15.54C15.15 15.93 14.52 15.93 14.13 15.54L5 6.41V9C5 9.55 4.55 10 4 10C3.45 10 3 9.55 3 9V4C3 3.44772 3.44772 3 4 3H9C9.55 3 10 3.45 10 4Z" fill="#737D8C"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

View file

@ -1,5 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 2L19 20" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 4H4.41122L17 16.615V19.415C16.9914 19.4057 16.9825 19.3965 16.9735 19.3874L1.98909 4.37176C1.93823 4.32079 1.88324 4.27646 1.82519 4.23876C2.18599 4.08505 2.58306 4 3 4ZM0.386676 5.52565C0.140502 5.96107 0 6.46413 0 7V17C0 18.6569 1.34315 20 3 20H14.7593L0.573407 5.78449C0.495634 5.70656 0.433392 5.619 0.386676 5.52565ZM17 7V13.7837L7.2367 4H14C15.6569 4 17 5.34315 17 7Z" fill="white"/>
<path d="M19 9L22.3753 6.29976C23.0301 5.77595 24 6.24212 24 7.08062V16.9194C24 17.7579 23.0301 18.2241 22.3753 17.7002L19 15V9Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 791 B

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 3L21 21" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6.59209V12C8 14.2091 9.79086 16 12 16C13.4616 16 14.7402 15.216 15.4381 14.0457L8 6.59209ZM16.8804 15.491C15.7918 17.0102 14.0115 18 12 18C8.68629 18 6 15.3137 6 12C6 11.4477 5.55228 11 5 11C4.44772 11 4 11.4477 4 12C4 16.0796 7.05369 19.446 11 19.9381V21C11 21.5523 11.4477 22 12 22C12.5523 22 13 21.5523 13 21V19.9381C15.1511 19.6699 17.037 18.5476 18.3077 16.9213L16.8804 15.491ZM19.3589 15.1433L17.7917 13.5729C17.9275 13.0716 18 12.5443 18 12C18 11.4477 18.4477 11 19 11C19.5523 11 20 11.4477 20 12C20 13.1159 19.7715 14.1783 19.3589 15.1433ZM16 11.7774L8.43077 4.19238C9.09091 2.89149 10.4413 2 12 2C14.2091 2 16 3.79086 16 6V11.7774Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 913 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 6C8 3.79086 9.79086 2 12 2C14.2091 2 16 3.79086 16 6V12C16 14.2091 14.2091 16 12 16C9.79086 16 8 14.2091 8 12V6ZM5 11C5.55228 11 6 11.4477 6 12C6 15.3137 8.68629 18 12 18C15.3137 18 18 15.3137 18 12C18 11.4477 18.4477 11 19 11C19.5523 11 20 11.4477 20 12C20 16.0796 16.9463 19.446 13 19.9381V21C13 21.5523 12.5523 22 12 22C11.4477 22 11 21.5523 11 21V19.9381C7.05369 19.446 4 16.0796 4 12C4 11.4477 4.44772 11 5 11Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 587 B

View file

@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 9C16 9 17 10.2857 17 12C17 13.7143 16 15 16 15" stroke="white" stroke-width="2" stroke-linecap="round"/>
<path d="M19 6C19 6 21 8.57143 21 12C21 15.4286 19 18 19 18" stroke="white" stroke-width="2" stroke-linecap="round"/>
<rect x="2" y="8" width="11" height="8" rx="2" fill="white"/>
<path d="M7 8L11.3598 4.36682C12.0111 3.82405 13 4.2872 13 5.13504V18.865C13 19.7128 12.0111 20.176 11.3598 19.6332L7 16V8Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 541 B

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="25px" height="26px" viewBox="-1 -1 25 26" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.4.3 (16618) - http://www.bohemiancoding.com/sketch -->
<title>Fill 72 + Path 98</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="05-Voice-and-video" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="05_2-Video-call" sketch:type="MSArtboardGroup" transform="translate(-910.000000, -719.000000)" stroke="#FF0064">
<g id="Fill-72-+-Path-98" sketch:type="MSLayerGroup" transform="translate(910.000000, 719.000000)">
<path d="M17.8404444,24 C15.8137778,24 10.8875556,21.408 6.75022222,15.8448889 C2.88088889,10.6413333 1,6.88222222 1,4.35244444 C1,2.36088889 2.37511111,1.41022222 3.11422222,0.9 L3.29644444,0.772888889 C4.11288889,0.188888889 5.38222222,0 5.86888889,0 C6.72222222,0 7.08177778,0.499555556 7.29955556,0.935111111 C7.48488889,1.30311111 9.01777778,4.59511111 9.17288889,5.00444444 C9.41111111,5.63377778 9.33288889,6.55111111 8.596,7.07822222 L8.46622222,7.16844444 C8.10044444,7.42222222 7.42,7.89333333 7.32577778,8.46622222 C7.28,8.74488889 7.37333333,9.03644444 7.61111111,9.35688889 C8.79777778,10.956 12.5862222,15.6506667 13.2693333,16.2884444 C13.8044444,16.7884444 14.4826667,16.8595556 14.9444444,16.4702222 C15.4222222,16.0675556 15.6342222,15.8297778 15.6364444,15.8271111 L15.6857778,15.7795556 C15.7257778,15.7457778 16.0991111,15.4497778 16.7093333,15.4497778 C17.1497778,15.4497778 17.5973333,15.6017778 18.04,15.9008889 C19.1884444,16.6768889 21.7808889,18.4106667 21.7808889,18.4106667 L21.8226667,18.4426667 C22.1542222,18.7266667 22.6333333,19.5453333 22.0751111,20.6106667 C21.496,21.7168889 19.6986667,24 17.8404444,24 L17.8404444,24 Z" id="Fill-72" sketch:type="MSShapeGroup"></path>
<path d="M19.8035085,4 L0,22.8035085" id="Path-98" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

17
res/img/voip/hangup.svg Normal file
View file

@ -0,0 +1,17 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="#FE2928"/>
</g>
<path d="m 24.008382,14.7565 c -1.6873,-0.0649 -5.157,0.373 -6.0006,0.5948 -0.0499,0.0131 -0.1074,0.0278 -0.1716,0.0442 -1.2952,0.3306 -5.35348,1.3663 -5.79196,4.6481 -0.33971,2.5426 1.36151,3.3122 2.21196,3.195 0.5886,-0.0738 2.2739,-0.3403 3.831,-0.6197 1.5291,-0.2743 1.5283,-1.283 1.5278,-1.9651 0,-0.0125 0,-0.025 0,-0.0373 v -1.3712 c 0,-0.3492 0.3281,-0.5511 0.7808,-0.6057 1.6024,-0.2176 2.9401,-0.2183 3.6097,-0.2183 h 0.0057 c 0.6695,0 1.9906,7e-4 3.593,0.2183 0.4527,0.0546 0.7808,0.2565 0.7808,0.6057 v 1.3712 c 0,0.0124 0,0.0248 0,0.0373 -5e-4,0.6821 -0.0013,1.6908 1.5278,1.9652 1.5571,0.2793 3.2424,0.5458 3.831,0.6196 0.8504,0.1172 2.5517,-0.6524 2.212,-3.195 -0.4385,-3.2818 -4.4968,-4.3175 -5.792,-4.6481 -0.0642,-0.0164 -0.1217,-0.031 -0.1716,-0.0442 -0.8435,-0.2218 -4.2966,-0.6597 -5.9838,-0.5948 z" fill="#ffffff" />
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

18
res/img/voip/mic-off.svg Normal file
View file

@ -0,0 +1,18 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="#61708B"/>
</g>
<path d="M15 11L33 29" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 14.5921V20C20 22.2091 21.7909 24 24 24C25.4616 24 26.7402 23.216 27.4381 22.0457L20 14.5921ZM28.8804 23.491C27.7918 25.0102 26.0115 26 24 26C20.6863 26 18 23.3137 18 20C18 19.4477 17.5523 19 17 19C16.4477 19 16 19.4477 16 20C16 24.0796 19.0537 27.446 23 27.9381V29C23 29.5523 23.4477 30 24 30C24.5523 30 25 29.5523 25 29V27.9381C27.1511 27.6699 29.037 26.5476 30.3077 24.9213L28.8804 23.491ZM31.3589 23.1433L29.7917 21.5729C29.9275 21.0716 30 20.5443 30 20C30 19.4477 30.4477 19 31 19C31.5523 19 32 19.4477 32 20C32 21.1159 31.7715 22.1783 31.3589 23.1433ZM28 19.7774L20.4308 12.1924C21.0909 10.8915 22.4413 10 24 10C26.2091 10 28 11.7909 28 14V19.7774Z" fill="white"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

17
res/img/voip/mic-on.svg Normal file
View file

@ -0,0 +1,17 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="white"/>
</g>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 14C20 11.7909 21.7909 10 24 10C26.2091 10 28 11.7909 28 14V20C28 22.2091 26.2091 24 24 24C21.7909 24 20 22.2091 20 20V14ZM17 19C17.5523 19 18 19.4477 18 20C18 23.3137 20.6863 26 24 26C27.3137 26 30 23.3137 30 20C30 19.4477 30.4477 19 31 19C31.5523 19 32 19.4477 32 20C32 24.0796 28.9463 27.446 25 27.9381V29C25 29.5523 24.5523 30 24 30C23.4477 30 23 29.5523 23 29V27.9381C19.0537 27.446 16 24.0796 16 20C16 19.4477 16.4477 19 17 19Z" fill="#61708B"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

19
res/img/voip/vid-off.svg Normal file
View file

@ -0,0 +1,19 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="#61708B"/>
</g>
<path d="M14.8334 11.6666L29.8334 26.6666" stroke="white" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 13.3333H17.676L28.1667 23.8458V26.1791C28.1595 26.1713 28.1521 26.1636 28.1446 26.1561L15.662 13.6474C16.0648 13.4464 16.5192 13.3333 17 13.3333ZM14.4359 14.7751C14.1593 15.2292 14 15.7626 14 16.3332V23.6666C14 25.3234 15.3431 26.6666 17 26.6666H26.2994L14.4778 14.8203C14.4632 14.8056 14.4492 14.7906 14.4359 14.7751ZM28.1667 16.3333V21.4863L20.0306 13.3333H25.1667C26.8235 13.3333 28.1667 14.6764 28.1667 16.3333Z" fill="white"/>
<path d="M29.8334 17.5L32.3753 15.4664C33.0301 14.9426 34 15.4087 34 16.2473V23.7527C34 24.5912 33.0301 25.0573 32.3753 24.5335L29.8334 22.5V17.5Z" fill="white"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

18
res/img/voip/vid-on.svg Normal file
View file

@ -0,0 +1,18 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="24" cy="20" r="20" fill="white"/>
</g>
<path d="M14 16.3334C14 14.6765 15.3431 13.3334 17 13.3334H25.1667C26.8235 13.3334 28.1667 14.6765 28.1667 16.3334V23.6667C28.1667 25.3236 26.8235 26.6667 25.1667 26.6667H17C15.3431 26.6667 14 25.3236 14 23.6667V16.3334Z" fill="#61708B"/>
<path d="M29.8334 17.5001L32.3753 15.4665C33.0301 14.9427 34 15.4089 34 16.2474V23.7528C34 24.5913 33.0301 25.0575 32.3753 24.5337L29.8334 22.5001V17.5001Z" fill="#61708B"/>
<defs>
<filter id="filter0_d" x="0" y="0" width="48" height="48" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.15 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -79,6 +79,7 @@ import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call"; import { MatrixCall, CallErrorCode, CallState, CallEvent, CallParty, CallType } from "matrix-js-sdk/src/webrtc/call";
import Analytics from './Analytics'; import Analytics from './Analytics';
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import {UIFeature} from "./settings/UIFeature";
enum AudioID { enum AudioID {
Ring = 'ringAudio', Ring = 'ringAudio',
@ -124,7 +125,7 @@ export default class CallHandler {
return window.mxCallHandler; return window.mxCallHandler;
} }
constructor() { start() {
dis.register(this.onAction); dis.register(this.onAction);
// add empty handlers for media actions, otherwise the media keys // add empty handlers for media actions, otherwise the media keys
// end up causing the audio elements with our ring/ringback etc // end up causing the audio elements with our ring/ringback etc
@ -137,6 +138,27 @@ export default class CallHandler {
navigator.mediaSession.setActionHandler('previoustrack', function() {}); navigator.mediaSession.setActionHandler('previoustrack', function() {});
navigator.mediaSession.setActionHandler('nexttrack', function() {}); navigator.mediaSession.setActionHandler('nexttrack', function() {});
} }
if (SettingsStore.getValue(UIFeature.Voip)) {
MatrixClientPeg.get().on('Call.incoming', this.onCallIncoming);
}
}
stop() {
const cli = MatrixClientPeg.get();
if (cli) {
cli.removeListener('Call.incoming', this.onCallIncoming);
}
}
private onCallIncoming = (call) => {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
} }
getCallForRoom(roomId: string): MatrixCall { getCallForRoom(roomId: string): MatrixCall {

View file

@ -48,6 +48,7 @@ import {Jitsi} from "./widgets/Jitsi";
import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform"; import {SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY} from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore"; import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import CallHandler from './CallHandler';
const HOMESERVER_URL_KEY = "mx_hs_url"; const HOMESERVER_URL_KEY = "mx_hs_url";
const ID_SERVER_URL_KEY = "mx_is_url"; const ID_SERVER_URL_KEY = "mx_is_url";
@ -665,6 +666,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.start();
CallHandler.sharedInstance().start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting // Start Mjolnir even though we haven't checked the feature flag yet. Starting
// the thing just wastes CPU cycles, but should result in no actual functionality // the thing just wastes CPU cycles, but should result in no actual functionality
@ -760,6 +762,7 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise<void
*/ */
export function stopMatrixClient(unsetClient = true): void { export function stopMatrixClient(unsetClient = true): void {
Notifier.stop(); Notifier.stop();
CallHandler.sharedInstance().stop();
UserActivity.sharedInstance().stop(); UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();

View file

@ -31,10 +31,26 @@ import {UPDATE_EVENT} from "../../stores/AsyncStore";
import {useEventEmitter} from "../../hooks/useEventEmitter"; import {useEventEmitter} from "../../hooks/useEventEmitter";
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader"; import MiniAvatarUploader, {AVATAR_SIZE} from "../views/elements/MiniAvatarUploader";
import Analytics from "../../Analytics";
import CountlyAnalytics from "../../CountlyAnalytics";
const onClickSendDm = () => dis.dispatch({action: 'view_create_chat'}); const onClickSendDm = () => {
const onClickExplore = () => dis.fire(Action.ViewRoomDirectory); Analytics.trackEvent('home_page', 'button', 'dm');
const onClickNewRoom = () => dis.dispatch({action: 'view_create_room'}); CountlyAnalytics.instance.track("home_page_button", { button: "dm" });
dis.dispatch({action: 'view_create_chat'});
};
const onClickExplore = () => {
Analytics.trackEvent('home_page', 'button', 'room_directory');
CountlyAnalytics.instance.track("home_page_button", { button: "room_directory" });
dis.fire(Action.ViewRoomDirectory);
};
const onClickNewRoom = () => {
Analytics.trackEvent('home_page', 'button', 'create_room');
CountlyAnalytics.instance.track("home_page_button", { button: "create_room" });
dis.dispatch({action: 'view_create_room'});
};
interface IProps { interface IProps {
justRegistered?: boolean; justRegistered?: boolean;

View file

@ -1353,18 +1353,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
}); });
if (SettingsStore.getValue(UIFeature.Voip)) {
cli.on('Call.incoming', function(call) {
// we dispatch this synchronously to make sure that the event
// handlers on the call are set up immediately (so that if
// we get an immediate hangup, we don't get a stuck call)
dis.dispatch({
action: 'incoming_call',
call: call,
}, true);
});
}
cli.on('Session.logged_out', function(errObj) { cli.on('Session.logged_out', function(errObj) {
if (Lifecycle.isLoggingOut()) return; if (Lifecycle.isLoggingOut()) return;

View file

@ -18,13 +18,11 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Matrix from 'matrix-js-sdk'; import Matrix from 'matrix-js-sdk';
import { _t, _td } from '../../languageHandler'; import { _t, _td } from '../../languageHandler';
import * as sdk from '../../index';
import {MatrixClientPeg} from '../../MatrixClientPeg'; import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend'; import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils'; import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import { CallState, CallType } from 'matrix-js-sdk/lib/webrtc/call';
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -42,13 +40,6 @@ export default class RoomStatusBar extends React.Component {
// the room this statusbar is representing. // the room this statusbar is representing.
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,
// The active call in the room, if any (means we show the call bar
// along with the status of the call)
callState: PropTypes.string,
// The type of the call in progress, or null if no call is in progress
callType: PropTypes.string,
// true if the room is being peeked at. This affects components that shouldn't // true if the room is being peeked at. This affects components that shouldn't
// logically be shown when peeking, such as a prompt to invite people to a room. // logically be shown when peeking, such as a prompt to invite people to a room.
isPeeking: PropTypes.bool, isPeeking: PropTypes.bool,
@ -115,12 +106,6 @@ export default class RoomStatusBar extends React.Component {
}); });
}; };
_showCallBar() {
return (this.props.callState &&
(this.props.callState !== CallState.Ended && this.props.callState !== CallState.Ringing)
);
}
_onResendAllClick = () => { _onResendAllClick = () => {
Resend.resendUnsentEvents(this.props.room); Resend.resendUnsentEvents(this.props.room);
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
@ -152,7 +137,7 @@ export default class RoomStatusBar extends React.Component {
// changed - so we use '0' to indicate normal size, and other values to // changed - so we use '0' to indicate normal size, and other values to
// indicate other sizes. // indicate other sizes.
_getSize() { _getSize() {
if (this._shouldShowConnectionError() || this._showCallBar()) { if (this._shouldShowConnectionError()) {
return STATUS_BAR_EXPANDED; return STATUS_BAR_EXPANDED;
} else if (this.state.unsentMessages.length > 0) { } else if (this.state.unsentMessages.length > 0) {
return STATUS_BAR_EXPANDED_LARGE; return STATUS_BAR_EXPANDED_LARGE;
@ -160,22 +145,6 @@ export default class RoomStatusBar extends React.Component {
return STATUS_BAR_HIDDEN; return STATUS_BAR_HIDDEN;
} }
// return suitable content for the image on the left of the status bar.
_getIndicator() {
if (this._showCallBar()) {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
return (
<TintableSvg src={require("../../../res/img/element-icons/room/in-call.svg")} width="23" height="20" />
);
}
if (this._shouldShowConnectionError()) {
return null;
}
return null;
}
_shouldShowConnectionError() { _shouldShowConnectionError() {
// no conn bar trumps the "some not sent" msg since you can't resend without // no conn bar trumps the "some not sent" msg since you can't resend without
// a connection! // a connection!
@ -266,25 +235,6 @@ export default class RoomStatusBar extends React.Component {
</div>; </div>;
} }
_getCallStatusText() {
switch (this.props.callState) {
case CallState.CreateOffer:
case CallState.InviteSent:
return _t('Calling...');
case CallState.Connecting:
case CallState.CreateAnswer:
return _t('Call connecting...');
case CallState.Connected:
return _t('Active call');
case CallState.WaitLocalMedia:
if (this.props.callType === CallType.Video) {
return _t('Starting camera...');
} else {
return _t('Starting microphone...');
}
}
}
// return suitable content for the main (text) part of the status bar. // return suitable content for the main (text) part of the status bar.
_getContent() { _getContent() {
if (this._shouldShowConnectionError()) { if (this._shouldShowConnectionError()) {
@ -307,26 +257,14 @@ export default class RoomStatusBar extends React.Component {
return this._getUnsentMessageContent(); return this._getUnsentMessageContent();
} }
if (this._showCallBar()) {
return (
<div className="mx_RoomStatusBar_callBar">
<b>{ this._getCallStatusText() }</b>
</div>
);
}
return null; return null;
} }
render() { render() {
const content = this._getContent(); const content = this._getContent();
const indicator = this._getIndicator();
return ( return (
<div className="mx_RoomStatusBar"> <div className="mx_RoomStatusBar">
<div className="mx_RoomStatusBar_indicator">
{ indicator }
</div>
<div role="alert"> <div role="alert">
{ content } { content }
</div> </div>

View file

@ -41,7 +41,7 @@ import rateLimitedFunc from '../../ratelimitedfunc';
import * as ObjectUtils from '../../ObjectUtils'; import * as ObjectUtils from '../../ObjectUtils';
import * as Rooms from '../../Rooms'; import * as Rooms from '../../Rooms';
import eventSearch, {searchPagination} from '../../Searching'; import eventSearch, {searchPagination} from '../../Searching';
import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, isOnlyCtrlOrCmdKeyEvent, Key} from '../../Keyboard'; import {isOnlyCtrlOrCmdIgnoreShiftKeyEvent, Key} from '../../Keyboard';
import MainSplit from './MainSplit'; import MainSplit from './MainSplit';
import RightPanel from './RightPanel'; import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore'; import RoomViewStore from '../../stores/RoomViewStore';
@ -56,7 +56,6 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils'; import {E2EStatus, shieldStatusForRoom} from '../../utils/ShieldUtils';
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import {SettingLevel} from "../../settings/SettingLevel"; import {SettingLevel} from "../../settings/SettingLevel";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import {IMatrixClientCreds} from "../../MatrixClientPeg"; import {IMatrixClientCreds} from "../../MatrixClientPeg";
import ScrollPanel from "./ScrollPanel"; import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
@ -68,10 +67,9 @@ import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel"; import PinnedEventsPanel from "../views/rooms/PinnedEventsPanel";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
import TintableSvg from "../views/elements/TintableSvg";
import {XOR} from "../../@types/common"; import {XOR} from "../../@types/common";
import { IThreepidInvite } from "../../stores/ThreepidInviteStore"; import { IThreepidInvite } from "../../stores/ThreepidInviteStore";
import { CallState, CallType, MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { CallState, MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import WidgetStore from "../../stores/WidgetStore"; import WidgetStore from "../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../stores/AsyncStore"; import {UPDATE_EVENT} from "../../stores/AsyncStore";
import Notifier from "../../Notifier"; import Notifier from "../../Notifier";
@ -508,8 +506,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.props.resizeNotifier.on("middlePanelResized", this.onResize); this.props.resizeNotifier.on("middlePanelResized", this.onResize);
} }
this.onResize(); this.onResize();
document.addEventListener("keydown", this.onNativeKeyDown);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
@ -592,8 +588,6 @@ export default class RoomView extends React.Component<IProps, IState> {
this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize); this.props.resizeNotifier.removeListener("middlePanelResized", this.onResize);
} }
document.removeEventListener("keydown", this.onNativeKeyDown);
// Remove RoomStore listener // Remove RoomStore listener
if (this.roomStoreToken) { if (this.roomStoreToken) {
this.roomStoreToken.remove(); this.roomStoreToken.remove();
@ -642,33 +636,6 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
}; };
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
private onNativeKeyDown = ev => {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
switch (ev.key) {
case Key.D:
if (ctrlCmdOnly) {
this.onMuteAudioClick();
handled = true;
}
break;
case Key.E:
if (ctrlCmdOnly) {
this.onMuteVideoClick();
handled = true;
}
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
};
private onReactKeyDown = ev => { private onReactKeyDown = ev => {
let handled = false; let handled = false;
@ -1324,10 +1291,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}; };
private onSettingsClick = () => { private onSettingsClick = () => {
dis.dispatch({ dis.dispatch({ action: "open_room_settings" });
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomSummary,
});
}; };
private onCancelClick = () => { private onCancelClick = () => {
@ -1758,8 +1722,6 @@ export default class RoomView extends React.Component<IProps, IState> {
isStatusAreaExpanded = this.state.statusBarVisible; isStatusAreaExpanded = this.state.statusBarVisible;
statusBar = <RoomStatusBar statusBar = <RoomStatusBar
room={this.state.room} room={this.state.room}
callState={this.state.callState}
callType={activeCall ? activeCall.type : null}
isPeeking={myMembership !== "join"} isPeeking={myMembership !== "join"}
onInviteClick={this.onInviteButtonClick} onInviteClick={this.onInviteButtonClick}
onVisible={this.onStatusBarVisible} onVisible={this.onStatusBarVisible}
@ -1883,56 +1845,6 @@ export default class RoomView extends React.Component<IProps, IState> {
}; };
} }
if (activeCall) {
let zoomButton; let videoMuteButton;
if (activeCall.type === CallType.Video) {
zoomButton = (
<div className="mx_RoomView_voipButton" onClick={this.onFullscreenClick} title={_t("Fill screen")}>
<TintableSvg
src={require("../../../res/img/element-icons/call/fullscreen.svg")}
width="29"
height="22"
style={{ marginTop: 1, marginRight: 4 }}
/>
</div>
);
videoMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteVideoClick}>
<TintableSvg
src={activeCall.isLocalVideoMuted() ?
require("../../../res/img/element-icons/call/video-muted.svg") :
require("../../../res/img/element-icons/call/video-call.svg")}
alt={activeCall.isLocalVideoMuted() ? _t("Click to unmute video") :
_t("Click to mute video")}
width=""
height="27"
/>
</div>;
}
const voiceMuteButton =
<div className="mx_RoomView_voipButton" onClick={this.onMuteAudioClick}>
<TintableSvg
src={activeCall.isMicrophoneMuted() ?
require("../../../res/img/element-icons/call/voice-muted.svg") :
require("../../../res/img/element-icons/call/voice-unmuted.svg")}
alt={activeCall.isMicrophoneMuted() ? _t("Click to unmute audio") : _t("Click to mute audio")}
width="21"
height="26"
/>
</div>;
// wrap the existing status bar into a 'callStatusBar' which adds more knobs.
statusBar =
<div className="mx_RoomView_callStatusBar">
{ voiceMuteButton }
{ videoMuteButton }
{ zoomButton }
{ statusBar }
</div>;
}
// if we have search results, we keep the messagepanel (so that it preserves its // if we have search results, we keep the messagepanel (so that it preserves its
// scroll state), but hide it. // scroll state), but hide it.
let searchResultsPanel; let searchResultsPanel;

View file

@ -1,28 +0,0 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
interface IProps {
}
const PulsedAvatar: React.FC<IProps> = (props) => {
return <div className="mx_PulsedAvatar">
{props.children}
</div>;
};
export default PulsedAvatar;

View file

@ -31,6 +31,7 @@ import {
ModalButtonKind, ModalButtonKind,
Widget, Widget,
WidgetApiFromWidgetAction, WidgetApiFromWidgetAction,
WidgetKind,
} from "matrix-widget-api"; } from "matrix-widget-api";
import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver"; import {StopGapWidgetDriver} from "../../../stores/widgets/StopGapWidgetDriver";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
@ -72,7 +73,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
} }
public componentDidMount() { public componentDidMount() {
const driver = new StopGapWidgetDriver( []); const driver = new StopGapWidgetDriver( [], this.widget, WidgetKind.Modal);
const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver); const messaging = new ClientWidgetApi(this.widget, this.appFrame.current, driver);
this.setState({messaging}); this.setState({messaging});
} }

View file

@ -0,0 +1,147 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import BaseDialog from "./BaseDialog";
import { _t } from "../../../languageHandler";
import { IDialogProps } from "./IDialogProps";
import {
Capability,
Widget,
WidgetEventCapability,
WidgetKind,
} from "matrix-widget-api";
import { objectShallowClone } from "../../../utils/objects";
import StyledCheckbox from "../elements/StyledCheckbox";
import DialogButtons from "../elements/DialogButtons";
import LabelledToggleSwitch from "../elements/LabelledToggleSwitch";
import { CapabilityText } from "../../../widgets/CapabilityText";
export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] {
return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]");
}
function setRememberedCapabilitiesForWidget(widget: Widget, caps: Capability[]) {
localStorage.setItem(`widget_${widget.id}_approved_caps`, JSON.stringify(caps));
}
interface IProps extends IDialogProps {
requestedCapabilities: Set<Capability>;
widget: Widget;
widgetKind: WidgetKind; // TODO: Refactor into the Widget class
}
interface IBooleanStates {
// @ts-ignore - TS wants a string key, but we know better
[capability: Capability]: boolean;
}
interface IState {
booleanStates: IBooleanStates;
rememberSelection: boolean;
}
export default class WidgetCapabilitiesPromptDialog extends React.PureComponent<IProps, IState> {
private eventPermissionsMap = new Map<Capability, WidgetEventCapability>();
constructor(props: IProps) {
super(props);
const parsedEvents = WidgetEventCapability.findEventCapabilities(this.props.requestedCapabilities);
parsedEvents.forEach(e => this.eventPermissionsMap.set(e.raw, e));
const states: IBooleanStates = {};
this.props.requestedCapabilities.forEach(c => states[c] = true);
this.state = {
booleanStates: states,
rememberSelection: true,
};
}
private onToggle = (capability: Capability) => {
const newStates = objectShallowClone(this.state.booleanStates);
newStates[capability] = !newStates[capability];
this.setState({booleanStates: newStates});
};
private onRememberSelectionChange = (newVal: boolean) => {
this.setState({rememberSelection: newVal});
};
private onSubmit = async (ev) => {
this.closeAndTryRemember(Object.entries(this.state.booleanStates)
.filter(([_, isSelected]) => isSelected)
.map(([cap]) => cap));
};
private onReject = async (ev) => {
this.closeAndTryRemember([]); // nothing was approved
};
private closeAndTryRemember(approved: Capability[]) {
if (this.state.rememberSelection) {
setRememberedCapabilitiesForWidget(this.props.widget, approved);
}
this.props.onFinished({approved});
}
public render() {
const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => {
const text = CapabilityText.for(cap, this.props.widgetKind);
const byline = text.byline
? <span className="mx_WidgetCapabilitiesPromptDialog_byline">{text.byline}</span>
: null;
return (
<div className="mx_WidgetCapabilitiesPromptDialog_cap" key={cap + i}>
<StyledCheckbox
checked={isChecked}
onChange={() => this.onToggle(cap)}
>{text.primary}</StyledCheckbox>
{byline}
</div>
);
});
return (
<BaseDialog
className="mx_WidgetCapabilitiesPromptDialog"
onFinished={this.props.onFinished}
title={_t("Approve widget permissions")}
>
<form onSubmit={this.onSubmit}>
<div className="mx_Dialog_content">
<div className="text-muted">{_t("This widget would like to:")}</div>
{checkboxRows}
<DialogButtons
primaryButton={_t("Approve")}
cancelButton={_t("Decline All")}
onPrimaryButtonClick={this.onSubmit}
onCancel={this.onReject}
additive={
<LabelledToggleSwitch
value={this.state.rememberSelection}
toggleInFront={true}
onChange={this.onRememberSelectionChange}
label={_t("Remember my selection for this widget")} />}
/>
</div>
</form>
</BaseDialog>
);
}
}

View file

@ -23,7 +23,6 @@ import PropTypes from 'prop-types';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import AccessibleButton from './AccessibleButton'; import AccessibleButton from './AccessibleButton';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index';
import AppPermission from './AppPermission'; import AppPermission from './AppPermission';
import AppWarning from './AppWarning'; import AppWarning from './AppWarning';
import Spinner from './Spinner'; import Spinner from './Spinner';
@ -375,11 +374,11 @@ export default class AppTile extends React.Component {
/> />
</div> </div>
); );
// if the widget would be allowed to remain on screen, we must put it in
// a PersistedElement from the get-go, otherwise the iframe will be // all widgets can theoretically be allowed to remain on screen, so we wrap
// re-mounted later when we do. // them all in a PersistedElement from the get-go. If we wait, the iframe will
if (this.props.whitelistCapabilities.includes('m.always_on_screen')) { // be re-mounted later, which means the widget has to start over, which is bad.
const PersistedElement = sdk.getComponent("elements.PersistedElement");
// Also wrap the PersistedElement in a div to fix the height, otherwise // Also wrap the PersistedElement in a div to fix the height, otherwise
// AppTile's border is in the wrong place // AppTile's border is in the wrong place
appTileBody = <div className="mx_AppTile_persistedWrapper"> appTileBody = <div className="mx_AppTile_persistedWrapper">
@ -389,7 +388,6 @@ export default class AppTile extends React.Component {
</div>; </div>;
} }
} }
}
let appTileClasses; let appTileClasses;
if (this.props.miniMode) { if (this.props.miniMode) {
@ -474,10 +472,6 @@ AppTile.propTypes = {
handleMinimisePointerEvents: PropTypes.bool, handleMinimisePointerEvents: PropTypes.bool,
// Optionally hide the popout widget icon // Optionally hide the popout widget icon
showPopout: PropTypes.bool, showPopout: PropTypes.bool,
// Widget capabilities to allow by default (without user confirmation)
// NOTE -- Use with caution. This is intended to aid better integration / UX
// basic widget capabilities, e.g. injecting sticker message events.
whitelistCapabilities: PropTypes.array,
// Is this an instance of a user widget // Is this an instance of a user widget
userWidget: PropTypes.bool, userWidget: PropTypes.bool,
}; };
@ -488,7 +482,6 @@ AppTile.defaultProps = {
showTitle: true, showTitle: true,
showPopout: true, showPopout: true,
handleMinimisePointerEvents: false, handleMinimisePointerEvents: false,
whitelistCapabilities: [],
userWidget: false, userWidget: false,
miniMode: false, miniMode: false,
}; };

View file

@ -54,6 +54,9 @@ export default class DialogButtons extends React.Component {
// disables only the primary button // disables only the primary button
primaryDisabled: PropTypes.bool, primaryDisabled: PropTypes.bool,
// something to stick next to the buttons, optionally
additive: PropTypes.element,
}; };
static defaultProps = { static defaultProps = {
@ -85,8 +88,14 @@ export default class DialogButtons extends React.Component {
</button>; </button>;
} }
let additive = null;
if (this.props.additive) {
additive = <div className="mx_Dialog_buttons_additive">{this.props.additive}</div>;
}
return ( return (
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
{ additive }
{ cancelButton } { cancelButton }
{ this.props.children } { this.props.children }
<button type={this.props.primaryIsSubmit ? 'submit' : 'button'} <button type={this.props.primaryIsSubmit ? 'submit' : 'button'}

View file

@ -21,6 +21,8 @@ import AccessibleButton from "./AccessibleButton";
import Tooltip from './Tooltip'; import Tooltip from './Tooltip';
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useTimeout} from "../../../hooks/useTimeout"; import {useTimeout} from "../../../hooks/useTimeout";
import Analytics from "../../../Analytics";
import CountlyAnalytics from '../../../CountlyAnalytics';
export const AVATAR_SIZE = 52; export const AVATAR_SIZE = 52;
@ -56,6 +58,8 @@ const MiniAvatarUploader: React.FC<IProps> = ({ hasAvatar, hasAvatarLabel, noAva
onChange={async (ev) => { onChange={async (ev) => {
if (!ev.target.files?.length) return; if (!ev.target.files?.length) return;
setBusy(true); setBusy(true);
Analytics.trackEvent("mini_avatar", "upload");
CountlyAnalytics.instance.track("mini_avatar_upload");
const file = ev.target.files[0]; const file = ev.target.files[0];
const uri = await cli.uploadContent(file); const uri = await cli.uploadContent(file);
await setAvatarUrl(uri); await setAvatarUrl(uri);

View file

@ -71,7 +71,6 @@ export default class PersistentApp extends React.Component {
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),
persistentWidgetInRoomId, appEvent.getId(), persistentWidgetInRoomId, appEvent.getId(),
); );
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, persistentWidgetInRoomId);
const AppTile = sdk.getComponent('elements.AppTile'); const AppTile = sdk.getComponent('elements.AppTile');
return <AppTile return <AppTile
key={app.id} key={app.id}
@ -82,7 +81,6 @@ export default class PersistentApp extends React.Component {
creatorUserId={app.creatorUserId} creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad} waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
miniMode={true} miniMode={true}
showMenubar={false} showMenubar={false}
/>; />;

View file

@ -103,7 +103,6 @@ const WidgetCard: React.FC<IProps> = ({ room, widgetId, onClose }) => {
creatorUserId={app.creatorUserId} creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad} waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, room.roomId)}
/> />
</BaseCard>; </BaseCard>;
}; };

View file

@ -210,8 +210,6 @@ export default class AppsDrawer extends React.Component {
if (!this.props.showApps) return <div />; if (!this.props.showApps) return <div />;
const apps = this.state.apps.map((app, index, arr) => { const apps = this.state.apps.map((app, index, arr) => {
const capWhitelist = WidgetUtils.getCapWhitelistForAppTypeInRoomId(app.type, this.props.room.roomId);
return (<AppTile return (<AppTile
key={app.id} key={app.id}
app={app} app={app}
@ -221,7 +219,6 @@ export default class AppsDrawer extends React.Component {
creatorUserId={app.creatorUserId} creatorUserId={app.creatorUserId}
widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)}
waitForIframeLoad={app.waitForIframeLoad} waitForIframeLoad={app.waitForIframeLoad}
whitelistCapabilities={capWhitelist}
/>); />);
}); });

View file

@ -29,9 +29,10 @@ import EditorStateTransfer from '../../../utils/EditorStateTransfer';
import classNames from 'classnames'; import classNames from 'classnames';
import {EventStatus} from 'matrix-js-sdk'; import {EventStatus} from 'matrix-js-sdk';
import BasicMessageComposer from "./BasicMessageComposer"; import BasicMessageComposer from "./BasicMessageComposer";
import {Key} from "../../../Keyboard"; import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
function _isReply(mxEvent) { function _isReply(mxEvent) {
@ -136,7 +137,10 @@ export default class EditMessageComposer extends React.Component {
if (event.metaKey || event.altKey || event.shiftKey) { if (event.metaKey || event.altKey || event.shiftKey) {
return; return;
} }
if (event.key === Key.ENTER) { const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
const send = ctrlEnterToSend ? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
: event.key === Key.ENTER;
if (send) {
this._sendEdit(); this._sendEdit();
event.preventDefault(); event.preventDefault();
} else if (event.key === Key.ESCAPE) { } else if (event.key === Key.ESCAPE) {

View file

@ -38,10 +38,11 @@ import * as sdk from '../../../index';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {_t, _td} from '../../../languageHandler'; import {_t, _td} from '../../../languageHandler';
import ContentMessages from '../../../ContentMessages'; import ContentMessages from '../../../ContentMessages';
import {Key} from "../../../Keyboard"; import {Key, isOnlyCtrlOrCmdKeyEvent} from "../../../Keyboard";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import RateLimitedFunc from '../../../ratelimitedfunc'; import RateLimitedFunc from '../../../ratelimitedfunc';
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import SettingsStore from "../../../settings/SettingsStore";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) { function addReplyToMessageContent(content, repliedToEvent, permalinkCreator) {
@ -122,7 +123,11 @@ export default class SendMessageComposer extends React.Component {
return; return;
} }
const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey; const hasModifier = event.altKey || event.ctrlKey || event.metaKey || event.shiftKey;
if (event.key === Key.ENTER && !hasModifier) { const ctrlEnterToSend = !!SettingsStore.getValue('MessageComposerInput.ctrlEnterToSend');
const send = ctrlEnterToSend
? event.key === Key.ENTER && isOnlyCtrlOrCmdKeyEvent(event)
: event.key === Key.ENTER && !hasModifier;
if (send) {
this._sendMessage(); this._sendMessage();
event.preventDefault(); event.preventDefault();
} else if (event.key === Key.ARROW_UP) { } else if (event.key === Key.ARROW_UP) {

View file

@ -280,7 +280,6 @@ export default class Stickerpicker extends React.Component {
showPopout={false} showPopout={false}
onMinimiseClick={this._onHideStickersClick} onMinimiseClick={this._onHideStickersClick}
handleMinimisePointerEvents={true} handleMinimisePointerEvents={true}
whitelistCapabilities={['m.sticker', 'visibility']}
userWidget={true} userWidget={true}
/> />
</PersistedElement> </PersistedElement>

View file

@ -33,6 +33,7 @@ export default class PreferencesUserSettingsTab extends React.Component {
'MessageComposerInput.autoReplaceEmoji', 'MessageComposerInput.autoReplaceEmoji',
'MessageComposerInput.suggestEmoji', 'MessageComposerInput.suggestEmoji',
'sendTypingNotifications', 'sendTypingNotifications',
'MessageComposerInput.ctrlEnterToSend',
]; ];
static TIMELINE_SETTINGS = [ static TIMELINE_SETTINGS = [

View file

@ -26,6 +26,15 @@ import PersistentApp from "../elements/PersistentApp";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { CallState, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
const SHOW_CALL_IN_STATES = [
CallState.Connected,
CallState.InviteSent,
CallState.Connecting,
CallState.CreateAnswer,
CallState.CreateOffer,
CallState.WaitLocalMedia,
];
interface IProps { interface IProps {
} }
@ -94,14 +103,13 @@ export default class CallPreview extends React.Component<IProps, IState> {
const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId); const callForRoom = CallHandler.sharedInstance().getCallForRoom(this.state.roomId);
const showCall = ( const showCall = (
this.state.activeCall && this.state.activeCall &&
this.state.activeCall.state === CallState.Connected && SHOW_CALL_IN_STATES.includes(this.state.activeCall.state) &&
!callForRoom !callForRoom
); );
if (showCall) { if (showCall) {
return ( return (
<CallView <CallView
className="mx_CallPreview"
onClick={this.onCallViewClick} onClick={this.onCallViewClick}
showHangup={true} showHangup={true}
/> />

View file

@ -21,12 +21,13 @@ import dis from '../../../dispatcher/dispatcher';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import AccessibleButton from '../elements/AccessibleButton';
import VideoFeed, { VideoFeedType } from "./VideoFeed"; import VideoFeed, { VideoFeedType } from "./VideoFeed";
import RoomAvatar from "../avatars/RoomAvatar"; import RoomAvatar from "../avatars/RoomAvatar";
import PulsedAvatar from '../avatars/PulsedAvatar';
import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { CallState, CallType, MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import { CallEvent } from 'matrix-js-sdk/src/webrtc/call'; import { CallEvent } from 'matrix-js-sdk/src/webrtc/call';
import classNames from 'classnames';
import AccessibleButton from '../elements/AccessibleButton';
import {isOnlyCtrlOrCmdKeyEvent, Key} from '../../../Keyboard';
interface IProps { interface IProps {
// js-sdk room object. If set, we will only show calls for the given // js-sdk room object. If set, we will only show calls for the given
@ -43,9 +44,6 @@ interface IProps {
// in a way that is likely to cause a resize. // in a way that is likely to cause a resize.
onResize?: any; onResize?: any;
// classname applied to view,
className?: string;
// Whether to show the hang up icon:W // Whether to show the hang up icon:W
showHangup?: boolean; showHangup?: boolean;
} }
@ -53,6 +51,10 @@ interface IProps {
interface IState { interface IState {
call: MatrixCall; call: MatrixCall;
isLocalOnHold: boolean, isLocalOnHold: boolean,
micMuted: boolean,
vidMuted: boolean,
callState: CallState,
controlsVisible: boolean,
} }
function getFullScreenElement() { function getFullScreenElement() {
@ -83,10 +85,15 @@ function exitFullscreen() {
if (exitMethod) exitMethod.call(document); if (exitMethod) exitMethod.call(document);
} }
const CONTROLS_HIDE_DELAY = 1000;
// Height of the header duplicated from CSS because we need to subtract it from our max
// height to get the max height of the video
const HEADER_HEIGHT = 44;
export default class CallView extends React.Component<IProps, IState> { export default class CallView extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private container = createRef<HTMLDivElement>(); private contentRef = createRef<HTMLDivElement>();
private controlsHideTimer: number = null;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -94,6 +101,10 @@ export default class CallView extends React.Component<IProps, IState> {
this.state = { this.state = {
call, call,
isLocalOnHold: call ? call.isLocalOnHold() : null, isLocalOnHold: call ? call.isLocalOnHold() : null,
micMuted: call ? call.isMicrophoneMuted() : null,
vidMuted: call ? call.isLocalVideoMuted() : null,
callState: call ? call.state : null,
controlsVisible: true,
} }
this.updateCallListeners(null, call); this.updateCallListeners(null, call);
@ -101,9 +112,11 @@ export default class CallView extends React.Component<IProps, IState> {
public componentDidMount() { public componentDidMount() {
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
document.addEventListener('keydown', this.onNativeKeyDown);
} }
public componentWillUnmount() { public componentWillUnmount() {
document.removeEventListener("keydown", this.onNativeKeyDown);
this.updateCallListeners(this.state.call, null); this.updateCallListeners(this.state.call, null);
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
} }
@ -111,11 +124,11 @@ export default class CallView extends React.Component<IProps, IState> {
private onAction = (payload) => { private onAction = (payload) => {
switch (payload.action) { switch (payload.action) {
case 'video_fullscreen': { case 'video_fullscreen': {
if (!this.container.current) { if (!this.contentRef.current) {
return; return;
} }
if (payload.fullscreen) { if (payload.fullscreen) {
requestFullscreen(this.container.current); requestFullscreen(this.contentRef.current);
} else if (getFullScreenElement()) { } else if (getFullScreenElement()) {
exitFullscreen(); exitFullscreen();
} }
@ -125,9 +138,21 @@ export default class CallView extends React.Component<IProps, IState> {
const newCall = this.getCall(); const newCall = this.getCall();
if (newCall !== this.state.call) { if (newCall !== this.state.call) {
this.updateCallListeners(this.state.call, newCall); this.updateCallListeners(this.state.call, newCall);
let newControlsVisible = this.state.controlsVisible;
if (newCall && !this.state.call) {
newControlsVisible = true;
if (this.controlsHideTimer !== null) {
clearTimeout(this.controlsHideTimer);
}
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
}
this.setState({ this.setState({
call: newCall, call: newCall,
isLocalOnHold: newCall ? newCall.isLocalOnHold() : null, isLocalOnHold: newCall ? newCall.isLocalOnHold() : null,
micMuted: newCall ? newCall.isMicrophoneMuted() : null,
vidMuted: newCall ? newCall.isLocalVideoMuted() : null,
callState: newCall ? newCall.state : null,
controlsVisible: newControlsVisible,
}); });
} }
if (!newCall && getFullScreenElement()) { if (!newCall && getFullScreenElement()) {
@ -144,11 +169,6 @@ export default class CallView extends React.Component<IProps, IState> {
if (this.props.room) { if (this.props.room) {
const roomId = this.props.room.roomId; const roomId = this.props.room.roomId;
call = CallHandler.sharedInstance().getCallForRoom(roomId); call = CallHandler.sharedInstance().getCallForRoom(roomId);
// We don't currently show voice calls in this view when in the room:
// they're represented in the room status bar at the bottom instead
// (but this will all change with the new designs)
if (call && call.type == CallType.Voice) call = null;
} else { } else {
call = CallHandler.sharedInstance().getAnyActiveCall(); call = CallHandler.sharedInstance().getAnyActiveCall();
// Ignore calls if we can't get the room associated with them. // Ignore calls if we can't get the room associated with them.
@ -160,7 +180,7 @@ export default class CallView extends React.Component<IProps, IState> {
} }
} }
if (call && call.state == CallState.Ended) return null; if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
return call; return call;
} }
@ -177,67 +197,240 @@ export default class CallView extends React.Component<IProps, IState> {
}); });
}; };
public render() { private onFullscreenClick = () => {
let view: React.ReactNode; dis.dispatch({
action: 'video_fullscreen',
fullscreen: true,
});
};
private onExpandClick = () => {
dis.dispatch({
action: 'view_room',
room_id: this.state.call.roomId,
});
};
private onControlsHideTimer = () => {
this.controlsHideTimer = null;
this.setState({
controlsVisible: false,
});
}
private onMouseMove = () => {
this.showControls();
}
private showControls() {
if (!this.state.controlsVisible) {
this.setState({
controlsVisible: true,
});
}
if (this.controlsHideTimer !== null) {
clearTimeout(this.controlsHideTimer);
}
this.controlsHideTimer = window.setTimeout(this.onControlsHideTimer, CONTROLS_HIDE_DELAY);
}
private onMicMuteClick = () => {
if (!this.state.call) return;
const newVal = !this.state.micMuted;
this.state.call.setMicrophoneMuted(newVal);
this.setState({micMuted: newVal});
}
private onVidMuteClick = () => {
if (!this.state.call) return;
const newVal = !this.state.vidMuted;
this.state.call.setLocalVideoMuted(newVal);
this.setState({vidMuted: newVal});
}
// we register global shortcuts here, they *must not conflict* with local shortcuts elsewhere or both will fire
// Note that this assumes we always have a callview on screen at any given time
// CallHandler would probably be a better place for this
private onNativeKeyDown = ev => {
let handled = false;
const ctrlCmdOnly = isOnlyCtrlOrCmdKeyEvent(ev);
switch (ev.key) {
case Key.D:
if (ctrlCmdOnly) {
this.onMicMuteClick();
// show the controls to give feedback
this.showControls();
handled = true;
}
break;
case Key.E:
if (ctrlCmdOnly) {
this.onVidMuteClick();
// show the controls to give feedback
this.showControls();
handled = true;
}
break;
}
if (handled) {
ev.stopPropagation();
ev.preventDefault();
}
};
private onRoomAvatarClick = () => {
dis.dispatch({
action: 'view_room',
room_id: this.state.call.roomId,
});
}
public render() {
if (!this.state.call) return null;
if (this.state.call) {
if (this.state.call.type === "voice") {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const callRoom = client.getRoom(this.state.call.roomId); const callRoom = client.getRoom(this.state.call.roomId);
let caption = _t("Active call"); let callControls;
if (this.state.isLocalOnHold) { if (this.props.room) {
// we currently have no UI for holding / unholding a call (apart from slash const micClasses = classNames({
// commands) so we don't disintguish between when we've put the call on hold mx_CallView_callControls_button: true,
// (ie. we'd show an unhold button) and when the other side has put us on hold mx_CallView_callControls_button_micOn: !this.state.micMuted,
// (where obviously we would not show such a button). mx_CallView_callControls_button_micOff: this.state.micMuted,
caption = _t("Call Paused"); });
}
view = <AccessibleButton className="mx_CallView_voice" onClick={this.props.onClick}> const vidClasses = classNames({
<PulsedAvatar> mx_CallView_callControls_button: true,
<RoomAvatar mx_CallView_callControls_button_vidOn: !this.state.vidMuted,
room={callRoom} mx_CallView_callControls_button_vidOff: this.state.vidMuted,
height={35} });
width={35}
// Put the other states of the mic/video icons in the document to make sure they're cached
// (otherwise the icon disappears briefly when toggled)
const micCacheClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_micOn: this.state.micMuted,
mx_CallView_callControls_button_micOff: !this.state.micMuted,
mx_CallView_callControls_button_invisible: true,
});
const vidCacheClasses = classNames({
mx_CallView_callControls_button: true,
mx_CallView_callControls_button_vidOn: this.state.micMuted,
mx_CallView_callControls_button_vidOff: !this.state.micMuted,
mx_CallView_callControls_button_invisible: true,
});
const callControlsClasses = classNames({
mx_CallView_callControls: true,
mx_CallView_callControls_hidden: !this.state.controlsVisible,
});
const vidMuteButton = this.state.call.type === CallType.Video ? <div
className={vidClasses}
onClick={this.onVidMuteClick}
/> : null;
callControls = <div className={callControlsClasses}>
<div
className={micClasses}
onClick={this.onMicMuteClick}
/> />
</PulsedAvatar> <div
<div> className="mx_CallView_callControls_button mx_CallView_callControls_button_hangup"
<h1>{callRoom.name}</h1>
<p>{ caption }</p>
</div>
</AccessibleButton>;
} else {
// For video calls, we currently ignore the call hold state altogether
// (the video will just go black)
// if we're fullscreen, we don't want to set a maxHeight on the video element.
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight;
view = <div className="mx_CallView_video" onClick={this.props.onClick}>
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
maxHeight={maxVideoHeight}
/>
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
</div>;
}
}
let hangup: React.ReactNode;
if (this.props.showHangup) {
hangup = <div
className="mx_CallView_hangup"
onClick={() => { onClick={() => {
dis.dispatch({ dis.dispatch({
action: 'hangup', action: 'hangup',
room_id: this.state.call.roomId, room_id: this.state.call.roomId,
}); });
}} }}
/>
{vidMuteButton}
<div className={micCacheClasses} />
<div className={vidCacheClasses} />
</div>;
}
// The 'content' for the call, ie. the videos for a video call and profile picture
// for voice calls (fills the bg)
let contentView: React.ReactNode;
if (this.state.call.type === CallType.Video) {
// if we're fullscreen, we don't want to set a maxHeight on the video element.
const maxVideoHeight = getFullScreenElement() ? null : this.props.maxVideoHeight - HEADER_HEIGHT;
contentView = <div className="mx_CallView_video" ref={this.contentRef} onMouseMove={this.onMouseMove}>
<VideoFeed type={VideoFeedType.Remote} call={this.state.call} onResize={this.props.onResize}
maxHeight={maxVideoHeight}
/>
<VideoFeed type={VideoFeedType.Local} call={this.state.call} />
{callControls}
</div>;
} else {
const avatarSize = this.props.room ? 200 : 75;
contentView = <div className="mx_CallView_voice" onMouseMove={this.onMouseMove}>
<RoomAvatar
room={callRoom}
height={avatarSize}
width={avatarSize}
/>
{callControls}
</div>;
}
const callTypeText = this.state.call.type === CallType.Video ? _t("Video Call") : _t("Voice Call");
let myClassName;
let fullScreenButton;
if (this.state.call.type === CallType.Video && this.props.room) {
fullScreenButton = <div className="mx_CallView_header_button mx_CallView_header_button_fullscreen"
onClick={this.onFullscreenClick} title={_t("Fill Screen")}
/>; />;
} }
return <div className={this.props.className} ref={this.container}> let expandButton;
{view} if (!this.props.room) {
{hangup} expandButton = <div className="mx_CallView_header_button mx_CallView_header_button_expand"
onClick={this.onExpandClick} title={_t("Return to call")}
/>;
}
const headerControls = <div className="mx_CallView_header_controls">
{fullScreenButton}
{expandButton}
</div>;
let header: React.ReactNode;
if (this.props.room) {
header = <div className="mx_CallView_header">
<div className="mx_CallView_header_phoneIcon"></div>
<span className="mx_CallView_header_callType">{callTypeText}</span>
{headerControls}
</div>;
myClassName = 'mx_CallView_large';
} else {
header = <div className="mx_CallView_header">
<AccessibleButton onClick={this.onRoomAvatarClick}>
<RoomAvatar room={callRoom} height={32} width={32} />
</AccessibleButton>
<div>
<div className="mx_CallView_header_roomName">{callRoom.name}</div>
<div className="mx_CallView_header_callTypeSmall">{callTypeText}</div>
</div>
{headerControls}
</div>;
myClassName = 'mx_CallView_pip';
}
return <div className={"mx_CallView " + myClassName}>
{header}
{contentView}
</div>; </div>;
} }
} }

View file

@ -22,7 +22,6 @@ import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { ActionPayload } from '../../../dispatcher/payloads'; import { ActionPayload } from '../../../dispatcher/payloads';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import PulsedAvatar from '../avatars/PulsedAvatar';
import RoomAvatar from '../avatars/RoomAvatar'; import RoomAvatar from '../avatars/RoomAvatar';
import FormButton from '../elements/FormButton'; import FormButton from '../elements/FormButton';
import { CallState } from 'matrix-js-sdk/lib/webrtc/call'; import { CallState } from 'matrix-js-sdk/lib/webrtc/call';
@ -108,13 +107,11 @@ export default class IncomingCallBox extends React.Component<IProps, IState> {
return <div className="mx_IncomingCallBox"> return <div className="mx_IncomingCallBox">
<div className="mx_IncomingCallBox_CallerInfo"> <div className="mx_IncomingCallBox_CallerInfo">
<PulsedAvatar>
<RoomAvatar <RoomAvatar
room={room} room={room}
height={32} height={32}
width={32} width={32}
/> />
</PulsedAvatar>
<div> <div>
<h1>{caller}</h1> <h1>{caller}</h1>
<p>{incomingCallText}</p> <p>{incomingCallText}</p>

View file

@ -73,8 +73,6 @@ export default class VideoFeed extends React.Component<IProps> {
let videoStyle = {}; let videoStyle = {};
if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight }; if (this.props.maxHeight) videoStyle = { maxHeight: this.props.maxHeight };
return <div className={classnames(videoClasses)}> return <video className={classnames(videoClasses)} ref={this.vid} style={videoStyle} />;
<video ref={this.vid} style={videoStyle}></video>
</div>;
} }
} }

View file

@ -569,6 +569,62 @@
"%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …", "%(names)s and %(count)s others are typing …|other": "%(names)s and %(count)s others are typing …",
"%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …", "%(names)s and %(count)s others are typing …|one": "%(names)s and one other is typing …",
"%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …", "%(names)s and %(lastPerson)s are typing …": "%(names)s and %(lastPerson)s are typing …",
"Remain on your screen when viewing another room, when running": "Remain on your screen when viewing another room, when running",
"Remain on your screen while running": "Remain on your screen while running",
"Send stickers into this room": "Send stickers into this room",
"Send stickers into your active room": "Send stickers into your active room",
"Change which room you're viewing": "Change which room you're viewing",
"Change the topic of this room": "Change the topic of this room",
"See when the topic changes in this room": "See when the topic changes in this room",
"Change the topic of your active room": "Change the topic of your active room",
"See when the topic changes in your active room": "See when the topic changes in your active room",
"Change the name of this room": "Change the name of this room",
"See when the name changes in this room": "See when the name changes in this room",
"Change the name of your active room": "Change the name of your active room",
"See when the name changes in your active room": "See when the name changes in your active room",
"Change the avatar of this room": "Change the avatar of this room",
"See when the avatar changes in this room": "See when the avatar changes in this room",
"Change the avatar of your active room": "Change the avatar of your active room",
"See when the avatar changes in your active room": "See when the avatar changes in your active room",
"Send stickers to this room as you": "Send stickers to this room as you",
"See when a sticker is posted in this room": "See when a sticker is posted in this room",
"Send stickers to your active room as you": "Send stickers to your active room as you",
"See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room",
"with an empty state key": "with an empty state key",
"with state key %(stateKey)s": "with state key %(stateKey)s",
"Send <b>%(eventType)s</b> events as you in this room": "Send <b>%(eventType)s</b> events as you in this room",
"See <b>%(eventType)s</b> events posted to this room": "See <b>%(eventType)s</b> events posted to this room",
"Send <b>%(eventType)s</b> events as you in your active room": "Send <b>%(eventType)s</b> events as you in your active room",
"See <b>%(eventType)s</b> events posted to your active room": "See <b>%(eventType)s</b> events posted to your active room",
"The <b>%(capability)s</b> capability": "The <b>%(capability)s</b> capability",
"Send messages as you in this room": "Send messages as you in this room",
"Send messages as you in your active room": "Send messages as you in your active room",
"See messages posted to this room": "See messages posted to this room",
"See messages posted to your active room": "See messages posted to your active room",
"Send text messages as you in this room": "Send text messages as you in this room",
"Send text messages as you in your active room": "Send text messages as you in your active room",
"See text messages posted to this room": "See text messages posted to this room",
"See text messages posted to your active room": "See text messages posted to your active room",
"Send emotes as you in this room": "Send emotes as you in this room",
"Send emotes as you in your active room": "Send emotes as you in your active room",
"See emotes posted to this room": "See emotes posted to this room",
"See emotes posted to your active room": "See emotes posted to your active room",
"Send images as you in this room": "Send images as you in this room",
"Send images as you in your active room": "Send images as you in your active room",
"See images posted to this room": "See images posted to this room",
"See images posted to your active room": "See images posted to your active room",
"Send videos as you in this room": "Send videos as you in this room",
"Send videos as you in your active room": "Send videos as you in your active room",
"See videos posted to this room": "See videos posted to this room",
"See videos posted to your active room": "See videos posted to your active room",
"Send general files as you in this room": "Send general files as you in this room",
"Send general files as you in your active room": "Send general files as you in your active room",
"See general files posted to this room": "See general files posted to this room",
"See general files posted to your active room": "See general files posted to your active room",
"Send <b>%(msgtype)s</b> messages as you in this room": "Send <b>%(msgtype)s</b> messages as you in this room",
"Send <b>%(msgtype)s</b> messages as you in your active room": "Send <b>%(msgtype)s</b> messages as you in your active room",
"See <b>%(msgtype)s</b> messages posted to this room": "See <b>%(msgtype)s</b> messages posted to this room",
"See <b>%(msgtype)s</b> messages posted to your active room": "See <b>%(msgtype)s</b> messages posted to your active room",
"Cannot reach homeserver": "Cannot reach homeserver", "Cannot reach homeserver": "Cannot reach homeserver",
"Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin", "Ensure you have a stable internet connection, or get in touch with the server admin": "Ensure you have a stable internet connection, or get in touch with the server admin",
"Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured", "Your %(brand)s is misconfigured": "Your %(brand)s is misconfigured",
@ -730,6 +786,8 @@
"Enable big emoji in chat": "Enable big emoji in chat", "Enable big emoji in chat": "Enable big emoji in chat",
"Send typing notifications": "Send typing notifications", "Send typing notifications": "Send typing notifications",
"Show typing notifications": "Show typing notifications", "Show typing notifications": "Show typing notifications",
"Use Command + Enter to send a message": "Use Command + Enter to send a message",
"Use Ctrl + Enter to send a message": "Use Ctrl + Enter to send a message",
"Automatically replace plain text Emoji": "Automatically replace plain text Emoji", "Automatically replace plain text Emoji": "Automatically replace plain text Emoji",
"Mirror local video feed": "Mirror local video feed", "Mirror local video feed": "Mirror local video feed",
"Enable Community Filter Panel": "Enable Community Filter Panel", "Enable Community Filter Panel": "Enable Community Filter Panel",
@ -778,8 +836,10 @@
"When rooms are upgraded": "When rooms are upgraded", "When rooms are upgraded": "When rooms are upgraded",
"My Ban List": "My Ban List", "My Ban List": "My Ban List",
"This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!", "This is your list of users/servers you have blocked - don't leave the room!": "This is your list of users/servers you have blocked - don't leave the room!",
"Active call": "Active call", "Video Call": "Video Call",
"Call Paused": "Call Paused", "Voice Call": "Voice Call",
"Fill Screen": "Fill Screen",
"Return to call": "Return to call",
"Unknown caller": "Unknown caller", "Unknown caller": "Unknown caller",
"Incoming voice call": "Incoming voice call", "Incoming voice call": "Incoming voice call",
"Incoming video call": "Incoming video call", "Incoming video call": "Incoming video call",
@ -2128,9 +2188,13 @@
"Upload Error": "Upload Error", "Upload Error": "Upload Error",
"Verify other session": "Verify other session", "Verify other session": "Verify other session",
"Verification Request": "Verification Request", "Verification Request": "Verification Request",
"Approve widget permissions": "Approve widget permissions",
"This widget would like to:": "This widget would like to:",
"Approve": "Approve",
"Decline All": "Decline All",
"Remember my selection for this widget": "Remember my selection for this widget",
"A widget would like to verify your identity": "A widget would like to verify your identity", "A widget would like to verify your identity": "A widget would like to verify your identity",
"A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.", "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.": "A widget located at %(widgetUrl)s would like to verify your identity. By allowing this, the widget will be able to verify your user ID, but not perform actions as you.",
"Remember my selection for this widget": "Remember my selection for this widget",
"Allow": "Allow", "Allow": "Allow",
"Deny": "Deny", "Deny": "Deny",
"Wrong file type": "Wrong file type", "Wrong file type": "Wrong file type",
@ -2380,10 +2444,6 @@
"%(count)s of your messages have not been sent.|one": "Your message was not sent.", "%(count)s of your messages have not been sent.|one": "Your message was not sent.",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.", "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|other": "<resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.",
"%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.", "%(count)s <resendText>Resend all</resendText> or <cancelText>cancel all</cancelText> now. You can also select individual messages to resend or cancel.|one": "<resendText>Resend message</resendText> or <cancelText>cancel message</cancelText> now.",
"Calling...": "Calling...",
"Call connecting...": "Call connecting...",
"Starting camera...": "Starting camera...",
"Starting microphone...": "Starting microphone...",
"Connectivity to the server has been lost.": "Connectivity to the server has been lost.", "Connectivity to the server has been lost.": "Connectivity to the server has been lost.",
"Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.", "Sent messages will be stored until your connection has returned.": "Sent messages will be stored until your connection has returned.",
"You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?", "You seem to be uploading files, are you sure you want to quit?": "You seem to be uploading files, are you sure you want to quit?",
@ -2395,11 +2455,6 @@
"Failed to reject invite": "Failed to reject invite", "Failed to reject invite": "Failed to reject invite",
"You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|other": "You have %(count)s unread notifications in a prior version of this room.",
"You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.", "You have %(count)s unread notifications in a prior version of this room.|one": "You have %(count)s unread notification in a prior version of this room.",
"Fill screen": "Fill screen",
"Click to unmute video": "Click to unmute video",
"Click to mute video": "Click to mute video",
"Click to unmute audio": "Click to unmute audio",
"Click to mute audio": "Click to mute audio",
"Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.",
"Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",
"Failed to load timeline position": "Failed to load timeline position", "Failed to load timeline position": "Failed to load timeline position",

View file

@ -103,6 +103,8 @@ export interface IVariables {
type Tags = Record<string, (sub: string) => React.ReactNode>; type Tags = Record<string, (sub: string) => React.ReactNode>;
export type TranslatedString = string | React.ReactNode;
/* /*
* Translates text and optionally also replaces XML-ish elements in the text with e.g. React components * Translates text and optionally also replaces XML-ish elements in the text with e.g. React components
* @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s". * @param {string} text The untranslated text, e.g "click <a>here</a> now to %(foo)s".
@ -121,7 +123,7 @@ type Tags = Record<string, (sub: string) => React.ReactNode>;
*/ */
export function _t(text: string, variables?: IVariables): string; export function _t(text: string, variables?: IVariables): string;
export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode; export function _t(text: string, variables: IVariables, tags: Tags): React.ReactNode;
export function _t(text: string, variables?: IVariables, tags?: Tags): string | React.ReactNode { export function _t(text: string, variables?: IVariables, tags?: Tags): TranslatedString {
// Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components // Don't do substitutions in counterpart. We handle it ourselves so we can replace with React components
// However, still pass the variables to counterpart so that it can choose the correct plural if count is given // However, still pass the variables to counterpart so that it can choose the correct plural if count is given
// It is enough to pass the count variable, but in the future counterpart might make use of other information too // It is enough to pass the count variable, but in the future counterpart might make use of other information too

View file

@ -32,6 +32,7 @@ import UseSystemFontController from './controllers/UseSystemFontController';
import { SettingLevel } from "./SettingLevel"; import { SettingLevel } from "./SettingLevel";
import SettingController from "./controllers/SettingController"; import SettingController from "./controllers/SettingController";
import { RightPanelPhases } from "../stores/RightPanelStorePhases"; import { RightPanelPhases } from "../stores/RightPanelStorePhases";
import { isMac } from '../Keyboard';
import UIFeatureController from "./controllers/UIFeatureController"; import UIFeatureController from "./controllers/UIFeatureController";
import { UIFeature } from "./UIFeature"; import { UIFeature } from "./UIFeature";
import { OrderedMultiController } from "./controllers/OrderedMultiController"; import { OrderedMultiController } from "./controllers/OrderedMultiController";
@ -324,6 +325,11 @@ export const SETTINGS: {[setting: string]: ISetting} = {
displayName: _td("Show typing notifications"), displayName: _td("Show typing notifications"),
default: true, default: true,
}, },
"MessageComposerInput.ctrlEnterToSend": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: isMac ? _td("Use Command + Enter to send a message") : _td("Use Ctrl + Enter to send a message"),
default: false,
},
"MessageComposerInput.autoReplaceEmoji": { "MessageComposerInput.autoReplaceEmoji": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS, supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Automatically replace plain text Emoji'), displayName: _td('Automatically replace plain text Emoji'),

View file

@ -14,8 +14,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { IWidgetApiRequest } from "matrix-widget-api";
export enum ElementWidgetActions { export enum ElementWidgetActions {
ClientReady = "im.vector.ready", ClientReady = "im.vector.ready",
HangupCall = "im.vector.hangup", HangupCall = "im.vector.hangup",
OpenIntegrationManager = "integration_manager_open", OpenIntegrationManager = "integration_manager_open",
ViewRoom = "io.element.view_room",
}
export interface IViewRoomApiRequest extends IWidgetApiRequest {
data: {
room_id: string; // eslint-disable-line camelcase
};
} }

View file

@ -0,0 +1,19 @@
/*
* Copyright 2020 The Matrix.org Foundation C.I.C.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export enum ElementWidgetCapabilities {
CanChangeViewedRoom = "io.element.view_room",
}

View file

@ -33,6 +33,8 @@ import {
WidgetApiToWidgetAction, WidgetApiToWidgetAction,
WidgetApiFromWidgetAction, WidgetApiFromWidgetAction,
IModalWidgetOpenRequest, IModalWidgetOpenRequest,
IWidgetApiErrorResponseData,
WidgetKind,
} from "matrix-widget-api"; } from "matrix-widget-api";
import { StopGapWidgetDriver } from "./StopGapWidgetDriver"; import { StopGapWidgetDriver } from "./StopGapWidgetDriver";
import { EventEmitter } from "events"; import { EventEmitter } from "events";
@ -47,13 +49,15 @@ import { WidgetType } from "../../widgets/WidgetType";
import ActiveWidgetStore from "../ActiveWidgetStore"; import ActiveWidgetStore from "../ActiveWidgetStore";
import { objectShallowClone } from "../../utils/objects"; import { objectShallowClone } from "../../utils/objects";
import defaultDispatcher from "../../dispatcher/dispatcher"; import defaultDispatcher from "../../dispatcher/dispatcher";
import { ElementWidgetActions } from "./ElementWidgetActions"; import { ElementWidgetActions, IViewRoomApiRequest } from "./ElementWidgetActions";
import Modal from "../../Modal"; import Modal from "../../Modal";
import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog"; import WidgetOpenIDPermissionsDialog from "../../components/views/dialogs/WidgetOpenIDPermissionsDialog";
import {ModalWidgetStore} from "../ModalWidgetStore"; import {ModalWidgetStore} from "../ModalWidgetStore";
import ThemeWatcher from "../../settings/watchers/ThemeWatcher"; import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import {getCustomTheme} from "../../theme"; import {getCustomTheme} from "../../theme";
import CountlyAnalytics from "../../CountlyAnalytics"; import CountlyAnalytics from "../../CountlyAnalytics";
import { ElementWidgetCapabilities } from "./ElementWidgetCapabilities";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
// TODO: Destroy all of this code // TODO: Destroy all of this code
@ -71,8 +75,8 @@ interface IAppTileProps {
// TODO: Don't use this because it's wrong // TODO: Don't use this because it's wrong
class ElementWidget extends Widget { class ElementWidget extends Widget {
constructor(w) { constructor(private rawDefinition: IWidget) {
super(w); super(rawDefinition);
} }
public get templateUrl(): string { public get templateUrl(): string {
@ -133,12 +137,7 @@ class ElementWidget extends Widget {
public getCompleteUrl(params: ITemplateParams, asPopout=false): string { public getCompleteUrl(params: ITemplateParams, asPopout=false): string {
return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, { return runTemplate(asPopout ? this.popoutTemplateUrl : this.templateUrl, {
// we need to supply a whole widget to the template, but don't have ...this.rawDefinition,
// easy access to the definition the superclass is using, so be sad
// and gutwrench it.
// This isn't a problem when the widget architecture is fixed and this
// subclass gets deleted.
...super['definition'], // XXX: Private member access
data: this.rawData, data: this.rawData,
}, params); }, params);
} }
@ -148,6 +147,8 @@ export class StopGapWidget extends EventEmitter {
private messaging: ClientWidgetApi; private messaging: ClientWidgetApi;
private mockWidget: ElementWidget; private mockWidget: ElementWidget;
private scalarToken: string; private scalarToken: string;
private roomId?: string;
private kind: WidgetKind;
constructor(private appTileProps: IAppTileProps) { constructor(private appTileProps: IAppTileProps) {
super(); super();
@ -160,6 +161,19 @@ export class StopGapWidget extends EventEmitter {
} }
this.mockWidget = new ElementWidget(app); this.mockWidget = new ElementWidget(app);
this.roomId = appTileProps.room?.roomId;
this.kind = appTileProps.userWidget ? WidgetKind.Account : WidgetKind.Room; // probably
}
private get eventListenerRoomId(): string {
// When widgets are listening to events, we need to make sure they're only
// receiving events for the right room. In particular, room widgets get locked
// to the room they were added in while account widgets listen to the currently
// active room.
if (this.roomId) return this.roomId;
return RoomViewStore.getRoomId();
} }
public get widgetApi(): ClientWidgetApi { public get widgetApi(): ClientWidgetApi {
@ -286,7 +300,8 @@ export class StopGapWidget extends EventEmitter {
public start(iframe: HTMLIFrameElement) { public start(iframe: HTMLIFrameElement) {
if (this.started) return; if (this.started) return;
const driver = new StopGapWidgetDriver( this.appTileProps.whitelistCapabilities || []); const allowedCapabilities = this.appTileProps.whitelistCapabilities || [];
const driver = new StopGapWidgetDriver( allowedCapabilities, this.mockWidget, this.kind);
this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver); this.messaging = new ClientWidgetApi(this.mockWidget, iframe, driver);
this.messaging.on("preparing", () => this.emit("preparing")); this.messaging.on("preparing", () => this.emit("preparing"));
this.messaging.on("ready", () => this.emit("ready")); this.messaging.on("ready", () => this.emit("ready"));
@ -298,18 +313,72 @@ export class StopGapWidget extends EventEmitter {
ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId); ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId);
} }
if (WidgetType.JITSI.matches(this.mockWidget.type)) { // Always attach a handler for ViewRoom, but permission check it internally
this.messaging.on("action:set_always_on_screen", this.messaging.on(`action:${ElementWidgetActions.ViewRoom}`, (ev: CustomEvent<IViewRoomApiRequest>) => {
ev.preventDefault(); // stop the widget API from auto-rejecting this
// Check up front if this is even a valid request
const targetRoomId = (ev.detail.data || {}).room_id;
if (!targetRoomId) {
return this.messaging.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {message: "Room ID not supplied."},
});
}
// Check the widget's permission
if (!this.messaging.hasCapability(ElementWidgetCapabilities.CanChangeViewedRoom)) {
return this.messaging.transport.reply(ev.detail, <IWidgetApiErrorResponseData>{
error: {message: "This widget does not have permission for this action (denied)."},
});
}
// at this point we can change rooms, so do that
defaultDispatcher.dispatch({
action: 'view_room',
room_id: targetRoomId,
});
// acknowledge so the widget doesn't freak out
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
});
// Attach listeners for feeding events - the underlying widget classes handle permissions for us
MatrixClientPeg.get().on('event', this.onEvent);
MatrixClientPeg.get().on('Event.decrypted', this.onEventDecrypted);
this.messaging.on(`action:${WidgetApiFromWidgetAction.UpdateAlwaysOnScreen}`,
(ev: CustomEvent<IStickyActionRequest>) => { (ev: CustomEvent<IStickyActionRequest>) => {
if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) { if (this.messaging.hasCapability(MatrixCapabilities.AlwaysOnScreen)) {
if (WidgetType.JITSI.matches(this.mockWidget.type)) {
CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true); CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true);
}
ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value); ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value);
ev.preventDefault(); ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
} }
}, },
); );
} else if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
// TODO: Replace this event listener with appropriate driver functionality once the API
// establishes a sane way to send events back and forth.
this.messaging.on(`action:${WidgetApiFromWidgetAction.SendSticker}`,
(ev: CustomEvent<IStickerActionRequest>) => {
if (this.messaging.hasCapability(MatrixCapabilities.StickerSending)) {
// Acknowledge first
ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
// Send the sticker
defaultDispatcher.dispatch({
action: 'm.sticker',
data: ev.detail.data,
widgetId: this.mockWidget.id,
});
}
},
);
if (WidgetType.STICKERPICKER.matches(this.mockWidget.type)) {
this.messaging.on(`action:${ElementWidgetActions.OpenIntegrationManager}`, this.messaging.on(`action:${ElementWidgetActions.OpenIntegrationManager}`,
(ev: CustomEvent<IWidgetApiRequest>) => { (ev: CustomEvent<IWidgetApiRequest>) => {
// Acknowledge first // Acknowledge first
@ -341,23 +410,6 @@ export class StopGapWidget extends EventEmitter {
} }
}, },
); );
// TODO: Replace this event listener with appropriate driver functionality once the API
// establishes a sane way to send events back and forth.
this.messaging.on(`action:${WidgetApiFromWidgetAction.SendSticker}`,
(ev: CustomEvent<IStickerActionRequest>) => {
// Acknowledge first
ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{});
// Send the sticker
defaultDispatcher.dispatch({
action: 'm.sticker',
data: ev.detail.data,
widgetId: this.mockWidget.id,
});
},
);
} }
} }
@ -391,5 +443,31 @@ export class StopGapWidget extends EventEmitter {
if (!this.started) return; if (!this.started) return;
WidgetMessagingStore.instance.stopMessaging(this.mockWidget); WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
ActiveWidgetStore.delRoomId(this.mockWidget.id); ActiveWidgetStore.delRoomId(this.mockWidget.id);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().off('event', this.onEvent);
MatrixClientPeg.get().off('Event.decrypted', this.onEventDecrypted);
}
}
private onEvent = (ev: MatrixEvent) => {
if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return;
if (ev.getRoomId() !== this.eventListenerRoomId) return;
this.feedEvent(ev);
};
private onEventDecrypted = (ev: MatrixEvent) => {
if (ev.isDecryptionFailure()) return;
if (ev.getRoomId() !== this.eventListenerRoomId) return;
this.feedEvent(ev);
};
private feedEvent(ev: MatrixEvent) {
if (!this.messaging) return;
const raw = ev.event;
this.messaging.feedEvent(raw).catch(e => {
console.error("Error sending event to widget: ", e);
});
} }
} }

View file

@ -14,17 +14,80 @@
* limitations under the License. * limitations under the License.
*/ */
import { Capability, WidgetDriver } from "matrix-widget-api"; import {
import { iterableUnion } from "../../utils/iterables"; Capability,
ISendEventDetails,
MatrixCapabilities,
Widget,
WidgetDriver,
WidgetKind,
} from "matrix-widget-api";
import { iterableDiff, iterableUnion } from "../../utils/iterables";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import ActiveRoomObserver from "../../ActiveRoomObserver";
import Modal from "../../Modal";
import WidgetCapabilitiesPromptDialog, {
getRememberedCapabilitiesForWidget,
} from "../../components/views/dialogs/WidgetCapabilitiesPromptDialog";
// TODO: Purge this from the universe // TODO: Purge this from the universe
export class StopGapWidgetDriver extends WidgetDriver { export class StopGapWidgetDriver extends WidgetDriver {
constructor(private allowedCapabilities: Capability[]) { private allowedCapabilities: Set<Capability>;
// TODO: Refactor widgetKind into the Widget class
constructor(allowedCapabilities: Capability[], private forWidget: Widget, private forWidgetKind: WidgetKind) {
super(); super();
// Always allow screenshots to be taken because it's a client-induced flow. The widget can't
// spew screenshots at us and can't request screenshots of us, so it's up to us to provide the
// button if the widget says it supports screenshots.
this.allowedCapabilities = new Set([...allowedCapabilities, MatrixCapabilities.Screenshots]);
} }
public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> { public async validateCapabilities(requested: Set<Capability>): Promise<Set<Capability>> {
return new Set(iterableUnion(requested, this.allowedCapabilities)); // Check to see if any capabilities aren't automatically accepted (such as sticker pickers
// allowing stickers to be sent). If there are excess capabilities to be approved, the user
// will be prompted to accept them.
const diff = iterableDiff(requested, this.allowedCapabilities);
const missing = new Set(diff.removed); // "removed" is "in A (requested) but not in B (allowed)"
const allowedSoFar = new Set(this.allowedCapabilities);
getRememberedCapabilitiesForWidget(this.forWidget).forEach(cap => allowedSoFar.add(cap));
// TODO: Do something when the widget requests new capabilities not yet asked for
if (missing.size > 0) {
try {
const [result] = await Modal.createTrackedDialog(
'Approve Widget Caps', '',
WidgetCapabilitiesPromptDialog,
{
requestedCapabilities: missing,
widget: this.forWidget,
widgetKind: this.forWidgetKind,
}).finished;
(result.approved || []).forEach(cap => allowedSoFar.add(cap));
} catch (e) {
console.error("Non-fatal error getting capabilities: ", e);
}
}
return new Set(iterableUnion(allowedSoFar, requested));
}
public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise<ISendEventDetails> {
const client = MatrixClientPeg.get();
const roomId = ActiveRoomObserver.activeRoomId;
if (!client || !roomId) throw new Error("Not in a room or not attached to a client");
let r: {event_id: string} = null; // eslint-disable-line camelcase
if (stateKey !== null) {
// state event
r = await client.sendStateEvent(roomId, eventType, content, stateKey);
} else {
// message event
r = await client.sendEvent(roomId, eventType, content);
}
return {roomId, eventId: r.event_id};
} }
} }

View file

@ -14,8 +14,12 @@
* limitations under the License. * limitations under the License.
*/ */
import { arrayUnion } from "./arrays"; import { arrayDiff, arrayUnion } from "./arrays";
export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> { export function iterableUnion<T>(a: Iterable<T>, b: Iterable<T>): Iterable<T> {
return arrayUnion(Array.from(a), Array.from(b)); return arrayUnion(Array.from(a), Array.from(b));
} }
export function iterableDiff<T>(a: Iterable<T>, b: Iterable<T>): { added: Iterable<T>, removed: Iterable<T> } {
return arrayDiff(Array.from(a), Array.from(b));
}

View file

@ -0,0 +1,342 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { Capability, EventDirection, MatrixCapabilities, WidgetEventCapability, WidgetKind } from "matrix-widget-api";
import { _t, _td, TranslatedString } from "../languageHandler";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities";
import React from "react";
type GENERIC_WIDGET_KIND = "generic";
const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic";
interface ISendRecvStaticCapText {
// @ts-ignore - TS wants the key to be a string, but we know better
[eventType: EventType]: {
// @ts-ignore - TS wants the key to be a string, but we know better
[widgetKind: WidgetKind | GENERIC_WIDGET_KIND]: {
// @ts-ignore - TS wants the key to be a string, but we know better
[direction: EventDirection]: string;
};
};
}
interface IStaticCapText {
// @ts-ignore - TS wants the key to be a string, but we know better
[capability: Capability]: {
// @ts-ignore - TS wants the key to be a string, but we know better
[widgetKind: WidgetKind | GENERIC_WIDGET_KIND]: string;
};
}
export interface TranslatedCapabilityText {
primary: TranslatedString;
byline?: TranslatedString;
}
export class CapabilityText {
private static simpleCaps: IStaticCapText = {
[MatrixCapabilities.AlwaysOnScreen]: {
[WidgetKind.Room]: _td("Remain on your screen when viewing another room, when running"),
[GENERIC_WIDGET_KIND]: _td("Remain on your screen while running"),
},
[MatrixCapabilities.StickerSending]: {
[WidgetKind.Room]: _td("Send stickers into this room"),
[GENERIC_WIDGET_KIND]: _td("Send stickers into your active room"),
},
[ElementWidgetCapabilities.CanChangeViewedRoom]: {
[GENERIC_WIDGET_KIND]: _td("Change which room you're viewing"),
},
};
private static stateSendRecvCaps: ISendRecvStaticCapText = {
[EventType.RoomTopic]: {
[WidgetKind.Room]: {
[EventDirection.Send]: _td("Change the topic of this room"),
[EventDirection.Receive]: _td("See when the topic changes in this room"),
},
[GENERIC_WIDGET_KIND]: {
[EventDirection.Send]: _td("Change the topic of your active room"),
[EventDirection.Receive]: _td("See when the topic changes in your active room"),
},
},
[EventType.RoomName]: {
[WidgetKind.Room]: {
[EventDirection.Send]: _td("Change the name of this room"),
[EventDirection.Receive]: _td("See when the name changes in this room"),
},
[GENERIC_WIDGET_KIND]: {
[EventDirection.Send]: _td("Change the name of your active room"),
[EventDirection.Receive]: _td("See when the name changes in your active room"),
},
},
[EventType.RoomAvatar]: {
[WidgetKind.Room]: {
[EventDirection.Send]: _td("Change the avatar of this room"),
[EventDirection.Receive]: _td("See when the avatar changes in this room"),
},
[GENERIC_WIDGET_KIND]: {
[EventDirection.Send]: _td("Change the avatar of your active room"),
[EventDirection.Receive]: _td("See when the avatar changes in your active room"),
},
},
};
private static nonStateSendRecvCaps: ISendRecvStaticCapText = {
[EventType.Sticker]: {
[WidgetKind.Room]: {
[EventDirection.Send]: _td("Send stickers to this room as you"),
[EventDirection.Receive]: _td("See when a sticker is posted in this room"),
},
[GENERIC_WIDGET_KIND]: {
[EventDirection.Send]: _td("Send stickers to your active room as you"),
[EventDirection.Receive]: _td("See when anyone posts a sticker to your active room"),
},
},
};
private static bylineFor(eventCap: WidgetEventCapability): TranslatedString {
if (eventCap.isState) {
return !eventCap.keyStr
? _t("with an empty state key")
: _t("with state key %(stateKey)s", {stateKey: eventCap.keyStr});
}
return null; // room messages are handled specially
}
public static for(capability: Capability, kind: WidgetKind): TranslatedCapabilityText {
// First see if we have a super simple line of text to provide back
if (CapabilityText.simpleCaps[capability]) {
const textForKind = CapabilityText.simpleCaps[capability];
if (textForKind[kind]) return {primary: _t(textForKind[kind])};
if (textForKind[GENERIC_WIDGET_KIND]) return {primary: _t(textForKind[GENERIC_WIDGET_KIND])};
// ... we'll fall through to the generic capability processing at the end of this
// function if we fail to locate a simple string and the capability isn't for an
// event.
}
// We didn't have a super simple line of text, so try processing the capability as the
// more complex event send/receive permission type.
const [eventCap] = WidgetEventCapability.findEventCapabilities([capability]);
if (eventCap) {
// Special case room messages so they show up a bit cleaner to the user. Result is
// effectively "Send images" instead of "Send messages... of type images" if we were
// to handle the msgtype nuances in this function.
if (!eventCap.isState && eventCap.eventType === EventType.RoomMessage) {
return CapabilityText.forRoomMessageCap(eventCap, kind);
}
// See if we have a static line of text to provide for the given event type and
// direction. The hope is that we do for common event types for friendlier copy.
const evSendRecv = eventCap.isState
? CapabilityText.stateSendRecvCaps
: CapabilityText.nonStateSendRecvCaps;
if (evSendRecv[eventCap.eventType]) {
const textForKind = evSendRecv[eventCap.eventType];
const textForDirection = textForKind[kind] || textForKind[GENERIC_WIDGET_KIND];
if (textForDirection && textForDirection[eventCap.direction]) {
return {
primary: _t(textForDirection[eventCap.direction]),
// no byline because we would have already represented the event properly
};
}
}
// We don't have anything simple, so just return a generic string for the event cap
if (kind === WidgetKind.Room) {
if (eventCap.direction === EventDirection.Send) {
return {
primary: _t("Send <b>%(eventType)s</b> events as you in this room", {
eventType: eventCap.eventType,
}, {
b: sub => <b>{sub}</b>,
}),
byline: CapabilityText.bylineFor(eventCap),
};
} else {
return {
primary: _t("See <b>%(eventType)s</b> events posted to this room", {
eventType: eventCap.eventType,
}, {
b: sub => <b>{sub}</b>,
}),
byline: CapabilityText.bylineFor(eventCap),
};
}
} else { // assume generic
if (eventCap.direction === EventDirection.Send) {
return {
primary: _t("Send <b>%(eventType)s</b> events as you in your active room", {
eventType: eventCap.eventType,
}, {
b: sub => <b>{sub}</b>,
}),
byline: CapabilityText.bylineFor(eventCap),
};
} else {
return {
primary: _t("See <b>%(eventType)s</b> events posted to your active room", {
eventType: eventCap.eventType,
}, {
b: sub => <b>{sub}</b>,
}),
byline: CapabilityText.bylineFor(eventCap),
};
}
}
}
// We don't have enough context to render this capability specially, so we'll present it as-is
return {
primary: _t("The <b>%(capability)s</b> capability", {capability}, {
b: sub => <b>{sub}</b>,
}),
};
}
private static forRoomMessageCap(eventCap: WidgetEventCapability, kind: WidgetKind): TranslatedCapabilityText {
// First handle the case of "all messages" to make the switch later on a bit clearer
if (!eventCap.keyStr) {
if (eventCap.direction === EventDirection.Send) {
return {
primary: kind === WidgetKind.Room
? _t("Send messages as you in this room")
: _t("Send messages as you in your active room"),
};
} else {
return {
primary: kind === WidgetKind.Room
? _t("See messages posted to this room")
: _t("See messages posted to your active room"),
};
}
}
// Now handle all the message types we care about. There are more message types available, however
// they are not as common so we don't bother rendering them. They'll fall into the generic case.
switch (eventCap.keyStr) {
case MsgType.Text: {
if (eventCap.direction === EventDirection.Send) {
return {
primary: kind === WidgetKind.Room
? _t("Send text messages as you in this room")
: _t("Send text messages as you in your active room"),
};
} else {
return {
primary: kind === WidgetKind.Room
? _t("See text messages posted to this room")
: _t("See text messages posted to your active room"),
};
}
}
case MsgType.Emote: {
if (eventCap.direction === EventDirection.Send) {
return {
primary: kind === WidgetKind.Room
? _t("Send emotes as you in this room")
: _t("Send emotes as you in your active room"),
};
} else {
return {
primary: kind === WidgetKind.Room
? _t("See emotes posted to this room")
: _t("See emotes posted to your active room"),
};
}
}
case MsgType.Image: {
if (eventCap.direction === EventDirection.Send) {
return {
primary: kind === WidgetKind.Room
? _t("Send images as you in this room")
: _t("Send images as you in your active room"),
};
} else {
return {
primary: kind === WidgetKind.Room
? _t("See images posted to this room")
: _t("See images posted to your active room"),
};
}
}
case MsgType.Video: {
if (eventCap.direction === EventDirection.Send) {
return {
primary: kind === WidgetKind.Room
? _t("Send videos as you in this room")
: _t("Send videos as you in your active room"),
};
} else {
return {
primary: kind === WidgetKind.Room
? _t("See videos posted to this room")
: _t("See videos posted to your active room"),
};
}
}
case MsgType.File: {
if (eventCap.direction === EventDirection.Send) {
return {
primary: kind === WidgetKind.Room
? _t("Send general files as you in this room")
: _t("Send general files as you in your active room"),
};
} else {
return {
primary: kind === WidgetKind.Room
? _t("See general files posted to this room")
: _t("See general files posted to your active room"),
};
}
}
default: {
let primary: TranslatedString;
if (eventCap.direction === EventDirection.Send) {
if (kind === WidgetKind.Room) {
primary = _t("Send <b>%(msgtype)s</b> messages as you in this room", {
msgtype: eventCap.keyStr,
}, {
b: sub => <b>{sub}</b>,
});
} else {
primary = _t("Send <b>%(msgtype)s</b> messages as you in your active room", {
msgtype: eventCap.keyStr,
}, {
b: sub => <b>{sub}</b>,
});
}
} else {
if (kind === WidgetKind.Room) {
primary = _t("See <b>%(msgtype)s</b> messages posted to this room", {
msgtype: eventCap.keyStr,
}, {
b: sub => <b>{sub}</b>,
});
} else {
primary = _t("See <b>%(msgtype)s</b> messages posted to your active room", {
msgtype: eventCap.keyStr,
}, {
b: sub => <b>{sub}</b>,
});
}
}
return {primary};
}
}
}
}

View file

@ -6506,8 +6506,8 @@ mathml-tag-names@^2.0.1:
integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": "matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop":
version "9.1.0" version "9.2.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/5ac00e346593f29f324b3af8e322928a6e1c427a" resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/6661bde6088e6e43f31198e8532432e162aef33c"
dependencies: dependencies:
"@babel/runtime" "^7.11.2" "@babel/runtime" "^7.11.2"
another-json "^0.2.0" another-json "^0.2.0"
@ -6532,10 +6532,10 @@ matrix-react-test-utils@^0.2.2:
resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853" resolved "https://registry.yarnpkg.com/matrix-react-test-utils/-/matrix-react-test-utils-0.2.2.tgz#c87144d3b910c7edc544a6699d13c7c2bf02f853"
integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ== integrity sha512-49+7gfV6smvBIVbeloql+37IeWMTD+fiywalwCqk8Dnz53zAFjKSltB3rmWHso1uecLtQEcPtCijfhzcLXAxTQ==
matrix-widget-api@^0.1.0-beta.8: matrix-widget-api@^0.1.0-beta.9:
version "0.1.0-beta.8" version "0.1.0-beta.9"
resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.8.tgz#17e85c03c46353373890b869b1fd46162bdb0026" resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.9.tgz#83952132c1610e013acb3e695f923f971ddd5637"
integrity sha512-sWqyWs0RQqny/BimZUOxUd9BTJBzQmJlJ1i3lsSh1JBygV+aK5xQsONL97fc4i6/nwQPK72uCVDF+HwTtkpAbQ== integrity sha512-nXo4iaquSya6hYLXccX8o1K960ckSQ0YXIubRDha+YmB+L09F5a7bUPS5JN2tYANOMzyfFAzWVuFwjHv4+K+rg==
dependencies: dependencies:
events "^3.2.0" events "^3.2.0"