Merge branch 'develop' into key-bindings

This commit is contained in:
Clemens Zeidler 2021-03-12 16:44:12 +13:00
commit 2a21d45ac0
388 changed files with 4301 additions and 1549 deletions

View file

@ -3,12 +3,15 @@ module.exports = {
"presets": [ "presets": [
["@babel/preset-env", { ["@babel/preset-env", {
"targets": [ "targets": [
"last 2 Chrome versions", "last 2 Firefox versions", "last 2 Safari versions" "last 2 Chrome versions",
"last 2 Firefox versions",
"last 2 Safari versions",
"last 2 Edge versions",
], ],
}], }],
"@babel/preset-typescript", "@babel/preset-typescript",
"@babel/preset-flow", "@babel/preset-flow",
"@babel/preset-react" "@babel/preset-react",
], ],
"plugins": [ "plugins": [
["@babel/plugin-proposal-decorators", {legacy: true}], ["@babel/plugin-proposal-decorators", {legacy: true}],
@ -18,6 +21,6 @@ module.exports = {
"@babel/plugin-proposal-object-rest-spread", "@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-transform-flow-comments", "@babel/plugin-transform-flow-comments",
"@babel/plugin-syntax-dynamic-import", "@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime" "@babel/plugin-transform-runtime",
] ],
}; };

View file

@ -489,54 +489,6 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
margin-top: 69px; margin-top: 69px;
} }
.mx_Beta {
color: red;
margin-right: 10px;
position: relative;
top: -3px;
background-color: white;
padding: 0 4px;
border-radius: 3px;
border: 1px solid darkred;
cursor: help;
transition-duration: 200ms;
font-size: smaller;
filter: opacity(0.5);
}
.mx_Beta:hover {
color: white;
border: 1px solid gray;
background-color: darkred;
}
.mx_TintableSvgButton {
position: relative;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
}
.mx_TintableSvgButton object {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
}
.mx_TintableSvgButton span {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0;
cursor: pointer;
}
// username colors // username colors
// used by SenderProfile & RoomPreviewBar // used by SenderProfile & RoomPreviewBar
.mx_Username_color1 { .mx_Username_color1 {
@ -606,6 +558,13 @@ input[type=text]:focus, input[type=password]:focus, textarea:focus {
} }
} }
@define-mixin ProgressBarBgColour $colour {
background-color: $colour;
&::-webkit-progress-bar {
background-color: $colour;
}
}
@define-mixin ProgressBarBorderRadius $radius { @define-mixin ProgressBarBorderRadius $radius {
border-radius: $radius; border-radius: $radius;
&::-moz-progress-bar { &::-moz-progress-bar {

View file

@ -18,6 +18,7 @@ limitations under the License.
display: flex; display: flex;
flex-direction: row; flex-direction: row;
min-width: 0; min-width: 0;
min-height: 0;
height: 100%; height: 100%;
} }

View file

@ -20,35 +20,54 @@ limitations under the License.
flex-direction: column; flex-direction: column;
} }
@keyframes mx_RoomView_fileDropTarget_animation {
from {
opacity: 0;
}
to {
opacity: 0.95;
}
}
.mx_RoomView_fileDropTarget { .mx_RoomView_fileDropTarget {
min-width: 0px; min-width: 0px;
width: 100%; width: 100%;
height: 100%;
font-size: $font-18px; font-size: $font-18px;
text-align: center; text-align: center;
pointer-events: none; pointer-events: none;
padding-left: 12px; background-color: $primary-bg-color;
padding-right: 12px; opacity: 0.95;
margin-left: -12px;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
background-color: $droptarget-bg-color;
border: 2px #e1dddd solid;
border-bottom: none;
position: absolute; position: absolute;
top: 52px;
bottom: 0px;
z-index: 3000; z-index: 3000;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
animation: mx_RoomView_fileDropTarget_animation;
animation-duration: 0.5s;
} }
.mx_RoomView_fileDropTargetLabel { @keyframes mx_RoomView_fileDropTarget_image_animation {
top: 50%; from {
width: 100%; width: 0px;
margin-top: -50px; }
position: absolute; to {
width: 32px;
}
}
.mx_RoomView_fileDropTarget_image {
animation: mx_RoomView_fileDropTarget_image_animation;
animation-duration: 0.5s;
margin-bottom: 16px;
} }
.mx_RoomView_auxPanel { .mx_RoomView_auxPanel {
@ -117,7 +136,6 @@ limitations under the License.
} }
.mx_RoomView_body { .mx_RoomView_body {
position: relative; //for .mx_RoomView_auxPanel_fullHeight
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;

View file

@ -203,8 +203,9 @@ limitations under the License.
.mx_SpaceRoomDirectory_actions { .mx_SpaceRoomDirectory_actions {
width: 180px; width: 180px;
text-align: right; text-align: right;
height: min-content; margin-left: 28px;
margin: auto 0 auto 28px; display: inline-flex;
align-items: center;
.mx_AccessibleButton { .mx_AccessibleButton {
vertical-align: middle; vertical-align: middle;
@ -223,9 +224,5 @@ limitations under the License.
line-height: $font-15px; line-height: $font-15px;
color: $secondary-fg-color; color: $secondary-fg-color;
} }
.mx_Checkbox {
display: inline-block;
}
} }
} }

View file

@ -20,6 +20,8 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_MainSplit > div:first-child { .mx_MainSplit > div:first-child {
padding: 80px 60px; padding: 80px 60px;
flex-grow: 1; flex-grow: 1;
max-height: 100%;
overflow-y: auto;
h1 { h1 {
margin: 0; margin: 0;
@ -69,9 +71,116 @@ $SpaceRoomViewInnerWidth: 428px;
} }
} }
.mx_SpaceRoomView_landing { .mx_SpaceRoomView_preview {
overflow-y: auto; padding: 32px 24px !important; // override default padding from above
margin: auto;
max-width: 480px;
box-sizing: border-box;
box-shadow: 2px 15px 30px $dialog-shadow-color;
border: 1px solid $input-border-color;
border-radius: 8px;
.mx_SpaceRoomView_preview_inviter {
display: flex;
align-items: center;
margin-bottom: 20px;
font-size: $font-15px;
> div {
margin-left: 8px;
.mx_SpaceRoomView_preview_inviter_name {
line-height: $font-18px;
}
.mx_SpaceRoomView_preview_inviter_mxid {
line-height: $font-24px;
color: $secondary-fg-color;
}
}
}
> .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px;
}
h1.mx_SpaceRoomView_preview_name {
margin: 20px 0 !important; // override default margin from above
}
.mx_SpaceRoomView_preview_info {
color: $tertiary-fg-color;
font-size: $font-15px;
line-height: $font-24px;
margin: 20px 0;
.mx_SpaceRoomView_preview_info_public,
.mx_SpaceRoomView_preview_info_private {
padding-left: 20px;
position: relative;
&::before {
position: absolute;
content: "";
width: 20px;
height: 20px;
top: 0;
left: -2px;
mask-position: center;
mask-repeat: no-repeat;
background-color: $tertiary-fg-color;
}
}
.mx_SpaceRoomView_preview_info_public::before {
mask-size: 12px;
mask-image: url("$(res)/img/globe.svg");
}
.mx_SpaceRoomView_preview_info_private::before {
mask-size: 14px;
mask-image: url("$(res)/img/element-icons/lock.svg");
}
.mx_AccessibleButton_kind_link {
color: inherit;
position: relative;
padding-left: 16px;
&::before {
content: "·"; // visual separator
position: absolute;
left: 6px;
}
}
}
.mx_SpaceRoomView_preview_topic {
font-size: $font-14px;
line-height: $font-22px;
color: $secondary-fg-color;
margin: 20px 0;
max-height: 160px;
overflow-y: auto;
}
.mx_SpaceRoomView_preview_joinButtons {
margin-top: 20px;
.mx_AccessibleButton {
width: 200px;
box-sizing: border-box;
padding: 14px 0;
& + .mx_AccessibleButton {
margin-left: 20px;
}
}
}
}
.mx_SpaceRoomView_landing {
> .mx_BaseAvatar_image, > .mx_BaseAvatar_image,
> .mx_BaseAvatar > .mx_BaseAvatar_image { > .mx_BaseAvatar > .mx_BaseAvatar_image {
border-radius: 12px; border-radius: 12px;
@ -128,14 +237,6 @@ $SpaceRoomViewInnerWidth: 428px;
font-size: $font-15px; font-size: $font-15px;
} }
.mx_SpaceRoomView_landing_joinButtons {
margin-top: 24px;
.mx_FormButton {
padding: 8px 22px;
}
}
.mx_SpaceRoomView_landing_adminButtons { .mx_SpaceRoomView_landing_adminButtons {
margin-top: 32px; margin-top: 32px;

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,47 +15,45 @@ limitations under the License.
*/ */
.mx_UploadBar { .mx_UploadBar {
padding-left: 65px; // line up with the shield area in the composer
position: relative; position: relative;
.mx_ProgressBar {
width: calc(100% - 40px); // cheating at a right margin
}
} }
.mx_UploadBar_uploadProgressOuter { .mx_UploadBar_filename {
height: 5px;
margin-left: 63px;
margin-top: -1px;
padding-bottom: 5px;
}
.mx_UploadBar_uploadProgressInner {
background-color: $accent-color;
height: 5px;
}
.mx_UploadBar_uploadFilename {
margin-top: 5px; margin-top: 5px;
margin-left: 65px; color: $muted-fg-color;
opacity: 0.5;
color: $primary-fg-color;
}
.mx_UploadBar_uploadIcon {
float: left;
margin-top: 5px;
margin-left: 14px;
}
.mx_UploadBar_uploadCancel {
float: right;
margin-top: 5px;
margin-right: 10px;
position: relative; position: relative;
opacity: 0.6; padding-left: 22px; // 18px for icon, 4px for padding
cursor: pointer; font-size: $font-15px;
z-index: 1; vertical-align: middle;
&::before {
content: "";
height: 18px;
width: 18px;
position: absolute;
top: 0;
left: 0;
mask-repeat: no-repeat;
mask-position: center;
background-color: $muted-fg-color;
mask-image: url('$(res)/img/element-icons/upload.svg');
}
} }
.mx_UploadBar_uploadBytes { .mx_UploadBar_cancel {
float: right; position: absolute;
margin-top: 5px; top: 0;
margin-right: 30px; right: 0;
color: $accent-color; height: 16px;
width: 16px;
margin-right: 16px; // align over rightmost button in composer
mask-repeat: no-repeat;
mask-position: center;
background-color: $muted-fg-color;
mask-image: url('$(res)/img/icons-close.svg');
} }

View file

@ -22,9 +22,18 @@ limitations under the License.
float: right; float: right;
} }
.mx_ViewSource_label_bottom { .mx_ViewSource_separator {
clear: both; clear: both;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid #e5e5e5;
padding-top: 0.7em;
padding-bottom: 0.7em;
}
.mx_ViewSource_heading {
font-size: $font-17px;
font-weight: 400;
color: $primary-fg-color;
margin-top: 0.7em;
} }
.mx_ViewSource pre { .mx_ViewSource pre {
@ -34,3 +43,7 @@ limitations under the License.
word-wrap: break-word; word-wrap: break-word;
white-space: pre-wrap; white-space: pre-wrap;
} }
.mx_ViewSource_details {
margin-top: 0.8em;
}

View file

@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// XXX: We shouldn't be using TemporaryTile anywhere - delete it. .mx_DecoratedRoomAvatar, .mx_ExtraTile {
.mx_DecoratedRoomAvatar, .mx_TemporaryTile {
position: relative; position: relative;
&.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar { &.mx_DecoratedRoomAvatar_cutout .mx_BaseAvatar {

View file

@ -19,6 +19,11 @@ limitations under the License.
max-width: 580px; max-width: 580px;
height: 80vh; height: 80vh;
max-height: 600px; max-height: 600px;
// Ensure dialog borders are always white as the HostSignupDialog
// does not yet support dark mode or theming in general.
// In the future we might want to pass the theme to the called
// iframe, should some hosting provider have that need.
background-color: #ffffff;
.mx_HostSignupDialog_info { .mx_HostSignupDialog_info {
text-align: center; text-align: center;

View file

@ -26,7 +26,9 @@ limitations under the License.
padding: 7px 18px; padding: 7px 18px;
text-align: center; text-align: center;
border-radius: 8px; border-radius: 8px;
display: inline-block; display: inline-flex;
align-items: center;
justify-content: center;
font-size: $font-14px; font-size: $font-14px;
} }

View file

@ -33,4 +33,10 @@ limitations under the License.
color: $notice-primary-color; color: $notice-primary-color;
background-color: $notice-primary-bg-color; background-color: $notice-primary-bg-color;
} }
&.mx_AccessibleButton_kind_secondary {
color: $secondary-fg-color;
border: 1px solid $secondary-fg-color;
background-color: unset;
}
} }

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2020 The Matrix.org Foundation C.I.C. Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -15,15 +15,15 @@ limitations under the License.
*/ */
progress.mx_ProgressBar { progress.mx_ProgressBar {
height: 4px; height: 6px;
width: 60px; width: 60px;
border-radius: 10px;
overflow: hidden; overflow: hidden;
appearance: none; appearance: none;
border: 0; border: none;
@mixin ProgressBarBorderRadius "10px"; @mixin ProgressBarBorderRadius "6px";
@mixin ProgressBarColour $accent-color; @mixin ProgressBarColour $progressbar-fg-color;
@mixin ProgressBarBgColour $progressbar-bg-color;
::-webkit-progress-value { ::-webkit-progress-value {
transition: width 1s; transition: width 1s;
} }

View file

@ -45,3 +45,46 @@ limitations under the License.
* big the content of the iframe is. */ * big the content of the iframe is. */
height: 1.5em; height: 1.5em;
} }
.mx_MFileBody_info {
background-color: $message-body-panel-bg-color;
border-radius: 4px;
width: 270px;
padding: 8px;
color: $message-body-panel-fg-color;
.mx_MFileBody_info_icon {
background-color: $message-body-panel-icon-bg-color;
border-radius: 20px;
display: inline-block;
width: 32px;
height: 32px;
position: relative;
vertical-align: middle;
margin-right: 12px;
&::before {
content: '';
mask-repeat: no-repeat;
mask-position: center;
mask-size: cover;
mask-image: url('$(res)/img/element-icons/room/composer/attach.svg');
background-color: $message-body-panel-fg-color;
width: 13px;
height: 15px;
position: absolute;
top: 8px;
left: 9px;
}
}
.mx_MFileBody_info_filename {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
width: calc(100% - 32px - 12px); // 32px icon, 12px margin on the icon
vertical-align: middle;
}
}

View file

@ -14,13 +14,11 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
.mx_UserInfo { .mx_EncryptionInfo_spinner {
.mx_EncryptionInfo_spinner { .mx_Spinner {
.mx_Spinner { margin-top: 25px;
margin-top: 25px; margin-bottom: 15px;
margin-bottom: 15px;
}
text-align: center;
} }
text-align: center;
} }

View file

@ -370,11 +370,6 @@ $MinWidth: 240px;
display: none; display: none;
} }
/* Avoid apptile iframes capturing mouse event focus when resizing */
.mx_AppsDrawer_resizing iframe {
pointer-events: none;
}
.mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper { .mx_AppsDrawer_resizing .mx_AppTile_persistedWrapper {
z-index: 1; z-index: 1;
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
.m_RoomView_auxPanel_stateViews { .m_RoomView_auxPanel_stateViews {
padding: 5px; padding: 5px;
padding-left: 19px; padding-left: 19px;
border-bottom: 1px solid #e5e5e5; border-bottom: 1px solid $primary-hairline-color;
} }
.m_RoomView_auxPanel_stateViews_span a { .m_RoomView_auxPanel_stateViews_span a {

View file

@ -213,23 +213,36 @@ $left-gutter: 64px;
color: $accent-fg-color; color: $accent-fg-color;
} }
.mx_EventTile_encrypting {
color: $event-encrypting-color !important;
}
.mx_EventTile_sending {
color: $event-sending-color;
}
.mx_EventTile_sending .mx_UserPill,
.mx_EventTile_sending .mx_RoomPill {
opacity: 0.5;
}
.mx_EventTile_notSent { .mx_EventTile_notSent {
color: $event-notsent-color; color: $event-notsent-color;
} }
.mx_EventTile_receiptSent,
.mx_EventTile_receiptSending {
// We don't use `position: relative` on the element because then it won't line
// up with the other read receipts
&::before {
background-color: $tertiary-fg-color;
mask-repeat: no-repeat;
mask-position: center;
mask-size: 14px;
width: 14px;
height: 14px;
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
}
}
.mx_EventTile_receiptSent::before {
mask-image: url('$(res)/img/element-icons/circle-sent.svg');
}
.mx_EventTile_receiptSending::before {
mask-image: url('$(res)/img/element-icons/circle-sending.svg');
}
.mx_EventTile_contextual { .mx_EventTile_contextual {
opacity: 0.4; opacity: 0.4;
} }

View file

@ -21,7 +21,7 @@ $left-gutter: 64px;
.mx_EventTile { .mx_EventTile {
> .mx_SenderProfile { > .mx_SenderProfile {
line-height: $font-20px; line-height: $font-20px;
padding-left: $left-gutter; margin-left: $left-gutter;
} }
> .mx_EventTile_line { > .mx_EventTile_line {

View file

@ -181,8 +181,7 @@ $irc-line-height: $font-18px;
> span { > span {
display: flex; display: flex;
> .mx_SenderProfile_name, > .mx_SenderProfile_name {
> .mx_SenderProfile_aux {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
min-width: var(--name-width); min-width: var(--name-width);
@ -212,8 +211,7 @@ $irc-line-height: $font-18px;
background: transparent; background: transparent;
> span { > span {
> .mx_SenderProfile_name, > .mx_SenderProfile_name {
> .mx_SenderProfile_aux {
min-width: inherit; min-width: inherit;
} }
} }

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="7.5" stroke="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 152 B

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM8 16C12.4183 16 16 12.4183 16 8C16 3.58172 12.4183 0 8 0C3.58172 0 0 3.58172 0 8C0 12.4183 3.58172 16 8 16Z" fill="#8D99A5"/>
<path d="M11.8697 4.95309C11.6784 4.7576 11.3597 4.74731 11.1578 4.93251L6.62066 9.04804L4.95244 7.91627C4.7293 7.77223 4.42116 7.77223 4.20865 7.95742C3.95363 8.1632 3.93238 8.5336 4.14489 8.78053L6.06813 10.9206C6.1 10.9515 6.13188 10.9926 6.17438 11.0132C6.53565 11.3013 7.07756 11.2498 7.37508 10.9L7.40695 10.8589L11.891 5.60128C12.0397 5.41608 12.0397 5.13828 11.8697 4.95309Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 784 B

View file

@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.99902 14L8.99902 4" stroke="black" stroke-width="1.5" stroke-linecap="round"/>
<path d="M12.5352 7.52441L8.99944 4.00012L5.46373 7.52441" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 336 B

View file

@ -1,19 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<svg width="45px" height="59px" viewBox="-1 -1 45 59" 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"> <path fill-rule="evenodd" clip-rule="evenodd" d="M16 0C7.16344 0 0 7.16344 0 16C0 24.8366 7.16344 32 16 32C24.8366 32 32 24.8366 32 16C32 7.16344 24.8366 0 16 0ZM17.2511 6.97409C16.9775 6.68236 16.5885 6.50012 16.157 6.50012C15.793 6.50012 15.4593 6.62973 15.1996 6.84532C15.1545 6.88267 15.1115 6.92281 15.0707 6.96564L8.79618 13.5539C8.22485 14.1538 8.24801 15.1032 8.8479 15.6746C9.4478 16.2459 10.3973 16.2227 10.9686 15.6228L14.657 11.7501V23.0589C14.657 23.8874 15.3285 24.5589 16.157 24.5589C16.9854 24.5589 17.657 23.8874 17.657 23.0589V11.7502L21.3452 15.6228C21.9165 16.2227 22.866 16.2459 23.4659 15.6746C24.0658 15.1032 24.0889 14.1538 23.5176 13.5539L17.2511 6.97409Z" fill="#0DBD8B"/>
<!-- Generator: bin/sketchtool 1.4 (305) - http://www.bohemiancoding.com/sketch -->
<title>icons_upload_drop</title>
<desc>Created with bin/sketchtool.</desc>
<defs></defs>
<g id="03-Input" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="03_05-File-drop" sketch:type="MSArtboardGroup" transform="translate(-570.000000, -368.000000)">
<g id="icons_upload_drop" sketch:type="MSLayerGroup" transform="translate(570.000000, 368.000000)">
<g id="Rectangle-5-+-Rectangle-6" sketch:type="MSShapeGroup">
<path d="M0,4.00812931 C0,1.79450062 1.78537926,0 4.00241155,0 L24.8253683,0 C24.8253683,0 42.2466793,16.8210687 42.2466793,16.8210687 L42.2466793,53.000599 C42.2466793,55.2094072 40.4583762,57 38.2531894,57 L3.99348992,57 C1.78794634,57 0,55.1999609 0,52.9918707 L0,4.00812931 Z" id="Rectangle-5" stroke="#76CFA6"></path>
<path d="M40.5848017,19.419576 L29.8354335,19.419576 C26.7387692,19.419576 24.2284269,16.9063989 24.2284269,13.8067771 L24.2284269,4.88501382 L40.5848017,19.419576 Z" id="Rectangle-6-Copy" fill="#FFFFFF"></path>
<path d="M42.2466793,18.3870968 L29.539478,18.3870968 C26.4130381,18.3870968 23.8785579,15.8497544 23.8785579,12.7203286 L23.8785579,0" id="Rectangle-6" stroke="#76CFA6"></path>
</g>
<path d="M31.3419737,32.9284726 C31.701384,32.9284726 32.0607942,32.8000473 32.3359677,32.5414375 C32.8825707,32.0259772 32.8825707,31.1920926 32.3359677,30.6766323 L21.622922,20.6119619 C21.076319,20.0965016 20.187153,20.0982608 19.638678,20.6102026 L8.9125289,30.6607991 C8.36405391,31.1762594 8.36218198,32.0119032 8.90878504,32.5273635 C9.4553881,33.0445831 10.344554,33.0445831 10.893029,32.530882 L19.2399573,24.7092556 L19.2437012,46.487014 C19.2437012,47.2153435 19.874541,47.8064516 20.6476474,47.8064516 C21.4244976,47.8064516 22.0515936,47.2153435 22.0515936,46.487014 L22.0478497,24.7426814 L30.3498517,32.5414375 C30.6231533,32.8000473 30.9825635,32.9284726 31.3419737,32.9284726 L31.3419737,32.9284726 Z" id="Fill-75" fill="#76CFA6" sketch:type="MSShapeGroup"></path>
</g>
</g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 802 B

View file

@ -138,9 +138,6 @@ $panel-divider-color: transparent;
$widget-menu-bar-bg-color: $header-panel-bg-color; $widget-menu-bar-bg-color: $header-panel-bg-color;
$widget-body-bg-color: rgba(141, 151, 165, 0.2); $widget-body-bg-color: rgba(141, 151, 165, 0.2);
// event tile lifecycle
$event-sending-color: $text-secondary-color;
// event redaction // event redaction
$event-redacted-fg-color: #606060; $event-redacted-fg-color: #606060;
$event-redacted-border-color: #000000; $event-redacted-border-color: #000000;
@ -173,6 +170,9 @@ $button-link-bg-color: transparent;
// Toggle switch // Toggle switch
$togglesw-off-color: $room-highlight-color; $togglesw-off-color: $room-highlight-color;
$progressbar-fg-color: $accent-color;
$progressbar-bg-color: #21262c;
$visual-bell-bg-color: #800; $visual-bell-bg-color: #800;
$room-warning-bg-color: $header-panel-bg-color; $room-warning-bg-color: $header-panel-bg-color;
@ -203,6 +203,10 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color; $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #21262c82;
$message-body-panel-icon-bg-color: #8e99a4;
$message-body-panel-fg-color: $primary-fg-color;
// Appearance tab colors // Appearance tab colors
$appearance-tab-border-color: $room-highlight-color; $appearance-tab-border-color: $room-highlight-color;

View file

@ -133,9 +133,6 @@ $panel-divider-color: $header-panel-border-color;
$widget-menu-bar-bg-color: $header-panel-bg-color; $widget-menu-bar-bg-color: $header-panel-bg-color;
$widget-body-bg-color: #1A1D23; $widget-body-bg-color: #1A1D23;
// event tile lifecycle
$event-sending-color: $text-secondary-color;
// event redaction // event redaction
$event-redacted-fg-color: #606060; $event-redacted-fg-color: #606060;
$event-redacted-border-color: #000000; $event-redacted-border-color: #000000;
@ -168,6 +165,9 @@ $button-link-bg-color: transparent;
// Toggle switch // Toggle switch
$togglesw-off-color: $room-highlight-color; $togglesw-off-color: $room-highlight-color;
$progressbar-fg-color: $accent-color;
$progressbar-bg-color: #21262c;
$visual-bell-bg-color: #800; $visual-bell-bg-color: #800;
$room-warning-bg-color: $header-panel-bg-color; $room-warning-bg-color: $header-panel-bg-color;
@ -198,6 +198,10 @@ $breadcrumb-placeholder-bg-color: #272c35;
$user-tile-hover-bg-color: $header-panel-bg-color; $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #21262c82;
$message-body-panel-icon-bg-color: #8e99a4;
$message-body-panel-fg-color: $primary-fg-color;
// Appearance tab colors // Appearance tab colors
$appearance-tab-border-color: $room-highlight-color; $appearance-tab-border-color: $room-highlight-color;

View file

@ -223,8 +223,6 @@ $widget-body-bg-color: #fff;
$yellow-background: #fff8e3; $yellow-background: #fff8e3;
// event tile lifecycle // event tile lifecycle
$event-encrypting-color: #abddbc;
$event-sending-color: #ddd;
$event-notsent-color: #f44; $event-notsent-color: #f44;
$event-highlight-fg-color: $warning-color; $event-highlight-fg-color: $warning-color;
@ -282,7 +280,8 @@ $togglesw-ball-color: #fff;
$slider-selection-color: $accent-color; $slider-selection-color: $accent-color;
$slider-background-color: #c1c9d6; $slider-background-color: #c1c9d6;
$progressbar-color: #000; $progressbar-fg-color: $accent-color;
$progressbar-bg-color: rgba(141, 151, 165, 0.2);
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
@ -322,6 +321,10 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color; $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #e3e8f082;
$message-body-panel-icon-bg-color: #ffffff;
$message-body-panel-fg-color: $muted-fg-color;
// FontSlider colors // FontSlider colors
$appearance-tab-border-color: $input-darker-bg-color; $appearance-tab-border-color: $input-darker-bg-color;

View file

@ -67,9 +67,6 @@ $groupFilterPanel-bg-color: rgba(232, 232, 232, 0.77);
// used by RoomDirectory permissions // used by RoomDirectory permissions
$plinth-bg-color: $secondary-accent-color; $plinth-bg-color: $secondary-accent-color;
// used by RoomDropTarget
$droptarget-bg-color: rgba(255,255,255,0.5);
// used by AddressSelector // used by AddressSelector
$selected-color: $secondary-accent-color; $selected-color: $secondary-accent-color;
@ -223,8 +220,6 @@ $widget-body-bg-color: #FFF;
$yellow-background: #fff8e3; $yellow-background: #fff8e3;
// event tile lifecycle // event tile lifecycle
$event-encrypting-color: #abddbc;
$event-sending-color: #ddd;
$event-notsent-color: #f44; $event-notsent-color: #f44;
$event-highlight-fg-color: $warning-color; $event-highlight-fg-color: $warning-color;
@ -282,7 +277,8 @@ $togglesw-ball-color: #fff;
$slider-selection-color: $accent-color; $slider-selection-color: $accent-color;
$slider-background-color: #c1c9d6; $slider-background-color: #c1c9d6;
$progressbar-color: #000; $progressbar-fg-color: $accent-color;
$progressbar-bg-color: rgba(141, 151, 165, 0.2);
$room-warning-bg-color: $yellow-background; $room-warning-bg-color: $yellow-background;
@ -323,6 +319,10 @@ $breadcrumb-placeholder-bg-color: #e8eef5;
$user-tile-hover-bg-color: $header-panel-bg-color; $user-tile-hover-bg-color: $header-panel-bg-color;
$message-body-panel-bg-color: #e3e8f082;
$message-body-panel-icon-bg-color: #ffffff;
$message-body-panel-fg-color: $muted-fg-color;
// FontSlider colors // FontSlider colors
$appearance-tab-border-color: $input-darker-bg-color; $appearance-tab-border-color: $input-darker-bg-color;

View file

@ -1,5 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
const fs = require('fs'); const fs = require('fs');
const { promises: fsp } = fs;
const path = require('path'); const path = require('path');
const glob = require('glob'); const glob = require('glob');
const util = require('util'); const util = require('util');
@ -25,6 +26,8 @@ async function reskindex() {
const header = args.h || args.header; const header = args.h || args.header;
const strm = fs.createWriteStream(componentIndexTmp); const strm = fs.createWriteStream(componentIndexTmp);
// Wait for the open event to ensure the file descriptor is set
await new Promise(resolve => strm.once("open", resolve));
if (header) { if (header) {
strm.write(fs.readFileSync(header)); strm.write(fs.readFileSync(header));
@ -53,14 +56,9 @@ async function reskindex() {
strm.write("export {components};\n"); strm.write("export {components};\n");
// Ensure the file has been fully written to disk before proceeding // Ensure the file has been fully written to disk before proceeding
await util.promisify(fs.fsync)(strm.fd);
await util.promisify(strm.end); await util.promisify(strm.end);
fs.rename(componentIndexTmp, componentIndex, function(err) { await fsp.rename(componentIndexTmp, componentIndex);
if (err) {
console.error("Error moving new index into place: " + err);
} else {
console.log('Reskindex: completed');
}
});
} }
// Expects both arrays of file names to be sorted // Expects both arrays of file names to be sorted
@ -77,9 +75,17 @@ function filesHaveChanged(files, prevFiles) {
return false; return false;
} }
// Wrapper since await at the top level is not well supported yet
function run() {
(async function() {
await reskindex();
console.log("Reskindex completed");
})();
}
// -w indicates watch mode where any FS events will trigger reskindex // -w indicates watch mode where any FS events will trigger reskindex
if (!args.w) { if (!args.w) {
reskindex(); run();
return; return;
} }
@ -87,5 +93,5 @@ let watchDebouncer = null;
chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => { chokidar.watch(path.join(componentsDir, componentJsGlob)).on('all', (event, path) => {
if (path === componentIndex) return; if (path === componentIndex) return;
if (watchDebouncer) clearTimeout(watchDebouncer); if (watchDebouncer) clearTimeout(watchDebouncer);
watchDebouncer = setTimeout(reskindex, 1000); watchDebouncer = setTimeout(run, 1000);
}); });

View file

@ -630,7 +630,7 @@ export default class CallHandler {
logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId); logger.debug("Mapped real room " + roomId + " to room ID " + mappedRoomId);
const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now(); const timeUntilTurnCresExpire = MatrixClientPeg.get().getTurnServersExpiry() - Date.now();
console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " seconds"); console.log("Current turn creds expire in " + timeUntilTurnCresExpire + " ms");
const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId); const call = createNewMatrixCall(MatrixClientPeg.get(), mappedRoomId);
this.calls.set(roomId, call); this.calls.set(roomId, call);

View file

@ -32,6 +32,14 @@ import Spinner from "./components/views/elements/Spinner";
import "blueimp-canvas-to-blob"; import "blueimp-canvas-to-blob";
import { Action } from "./dispatcher/actions"; import { Action } from "./dispatcher/actions";
import CountlyAnalytics from "./CountlyAnalytics"; import CountlyAnalytics from "./CountlyAnalytics";
import {
UploadCanceledPayload,
UploadErrorPayload,
UploadFinishedPayload,
UploadProgressPayload,
UploadStartedPayload,
} from "./dispatcher/payloads/UploadPayload";
import {IUpload} from "./models/IUpload";
const MAX_WIDTH = 800; const MAX_WIDTH = 800;
const MAX_HEIGHT = 600; const MAX_HEIGHT = 600;
@ -44,15 +52,6 @@ export class UploadCanceledError extends Error {}
type ThumbnailableElement = HTMLImageElement | HTMLVideoElement; type ThumbnailableElement = HTMLImageElement | HTMLVideoElement;
interface IUpload {
fileName: string;
roomId: string;
total: number;
loaded: number;
promise: Promise<any>;
canceled?: boolean;
}
interface IMediaConfig { interface IMediaConfig {
"m.upload.size"?: number; "m.upload.size"?: number;
} }
@ -478,7 +477,7 @@ export default class ContentMessages {
if (upload) { if (upload) {
upload.canceled = true; upload.canceled = true;
MatrixClientPeg.get().cancelUpload(upload.promise); MatrixClientPeg.get().cancelUpload(upload.promise);
dis.dispatch({action: 'upload_canceled', upload}); dis.dispatch<UploadCanceledPayload>({action: Action.UploadCanceled, upload});
} }
} }
@ -539,7 +538,7 @@ export default class ContentMessages {
promise: prom, promise: prom,
}; };
this.inprogress.push(upload); this.inprogress.push(upload);
dis.dispatch({action: 'upload_started'}); dis.dispatch<UploadStartedPayload>({action: Action.UploadStarted, upload});
// Focus the composer view // Focus the composer view
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
@ -547,7 +546,7 @@ export default class ContentMessages {
function onProgress(ev) { function onProgress(ev) {
upload.total = ev.total; upload.total = ev.total;
upload.loaded = ev.loaded; upload.loaded = ev.loaded;
dis.dispatch({action: 'upload_progress', upload: upload}); dis.dispatch<UploadProgressPayload>({action: Action.UploadProgress, upload});
} }
let error; let error;
@ -601,9 +600,9 @@ export default class ContentMessages {
if (error && error.http_status === 413) { if (error && error.http_status === 413) {
this.mediaConfig = null; this.mediaConfig = null;
} }
dis.dispatch({action: 'upload_failed', upload, error}); dis.dispatch<UploadErrorPayload>({action: Action.UploadFailed, upload, error});
} else { } else {
dis.dispatch({action: 'upload_finished', upload}); dis.dispatch<UploadFinishedPayload>({action: Action.UploadFinished, upload});
dis.dispatch({action: 'message_sent'}); dis.dispatch({action: 'message_sent'});
} }
}); });

55
src/Livestream.ts Normal file
View file

@ -0,0 +1,55 @@
/*
Copyright 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { ClientWidgetApi } from "matrix-widget-api";
import { MatrixClientPeg } from "./MatrixClientPeg";
import SdkConfig from "./SdkConfig";
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
export function getConfigLivestreamUrl() {
return SdkConfig.get()["audioStreamUrl"];
}
// Dummy rtmp URL used to signal that we want a special audio-only stream
const AUDIOSTREAM_DUMMY_URL = 'rtmp://audiostream.dummy/';
async function createLiveStream(roomId: string) {
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
const url = getConfigLivestreamUrl() + "/createStream";
const response = await window.fetch(url, {
method: 'POST',
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
room_id: roomId,
openid_token: openIdToken,
}),
});
const respBody = await response.json();
return respBody['stream_id'];
}
export async function startJitsiAudioLivestream(widgetMessaging: ClientWidgetApi, roomId: string) {
const streamId = await createLiveStream(roomId);
await widgetMessaging.transport.send(ElementWidgetActions.StartLiveStream, {
rtmpStreamKey: AUDIOSTREAM_DUMMY_URL + streamId,
});
}

View file

@ -99,9 +99,9 @@ class Presence {
try { try {
await MatrixClientPeg.get().setPresence(this.state); await MatrixClientPeg.get().setPresence(this.state);
console.info("Presence: %s", newState); console.info("Presence:", newState);
} catch (err) { } catch (err) {
console.error("Failed to set presence: %s", err); console.error("Failed to set presence:", err);
this.state = oldState; this.state = oldState;
} }
} }

View file

@ -22,7 +22,7 @@ import MultiInviter from './utils/MultiInviter';
import Modal from './Modal'; import Modal from './Modal';
import * as sdk from './'; import * as sdk from './';
import { _t } from './languageHandler'; import { _t } from './languageHandler';
import InviteDialog, {KIND_DM, KIND_INVITE, KIND_SPACE_INVITE} from "./components/views/dialogs/InviteDialog"; import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog";
import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog";
import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore";
@ -50,11 +50,10 @@ export function showStartChatInviteDialog(initialText) {
} }
export function showRoomInviteDialog(roomId) { export function showRoomInviteDialog(roomId) {
const isSpace = MatrixClientPeg.get()?.getRoom(roomId)?.isSpaceRoom();
// This dialog handles the room creation internally - we don't need to worry about it. // This dialog handles the room creation internally - we don't need to worry about it.
Modal.createTrackedDialog( Modal.createTrackedDialog(
"Invite Users", isSpace ? "Space" : "Room", InviteDialog, { "Invite Users", "", InviteDialog, {
kind: isSpace ? KIND_SPACE_INVITE : KIND_INVITE, kind: KIND_INVITE,
roomId, roomId,
}, },
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true,

View file

@ -23,7 +23,7 @@ class Skinner {
if (!name) throw new Error(`Invalid component name: ${name}`); if (!name) throw new Error(`Invalid component name: ${name}`);
if (this.components === null) { if (this.components === null) {
throw new Error( throw new Error(
"Attempted to get a component before a skin has been loaded."+ `Attempted to get a component (${name}) before a skin has been loaded.`+
" This is probably because either:"+ " This is probably because either:"+
" a) Your app has not called sdk.loadSkin(), or"+ " a) Your app has not called sdk.loadSkin(), or"+
" b) A component has called getComponent at the root level", " b) A component has called getComponent at the root level",

View file

@ -441,15 +441,14 @@ export const Commands = [
}), }),
new Command({ new Command({
command: 'invite', command: 'invite',
args: '<user-id>', args: '<user-id> [<reason>]',
description: _td('Invites user with given id to current room'), description: _td('Invites user with given id to current room'),
runFn: function(roomId, args) { runFn: function(roomId, args) {
if (args) { if (args) {
const matches = args.match(/^(\S+)$/); const [address, reason] = args.split(/\s+(.+)/);
if (matches) { if (address) {
// We use a MultiInviter to re-use the invite logic, even though // We use a MultiInviter to re-use the invite logic, even though
// we're only inviting one user. // we're only inviting one user.
const address = matches[1];
// If we need an identity server but don't have one, things // If we need an identity server but don't have one, things
// get a bit more complex here, but we try to show something // get a bit more complex here, but we try to show something
// meaningful. // meaningful.
@ -490,7 +489,7 @@ export const Commands = [
} }
const inviter = new MultiInviter(roomId); const inviter = new MultiInviter(roomId);
return success(prom.then(() => { return success(prom.then(() => {
return inviter.invite([address]); return inviter.invite([address], reason);
}).then(() => { }).then(() => {
if (inviter.getCompletionState(address) !== "invited") { if (inviter.getCompletionState(address) !== "invited") {
throw new Error(inviter.getErrorText(address)); throw new Error(inviter.getErrorText(address));

View file

@ -37,7 +37,7 @@ export default class VoipUserMapper {
return results[0].userid; return results[0].userid;
} }
public async getOrCreateVirtualRoomForRoom(roomId: string):Promise<string> { public async getOrCreateVirtualRoomForRoom(roomId: string): Promise<string> {
const userId = DMRoomMap.shared().getUserIdForRoomId(roomId); const userId = DMRoomMap.shared().getUserIdForRoomId(roomId);
if (!userId) return null; if (!userId) return null;
@ -52,7 +52,7 @@ export default class VoipUserMapper {
return virtualRoomId; return virtualRoomId;
} }
public nativeRoomForVirtualRoom(roomId: string):string { public nativeRoomForVirtualRoom(roomId: string): string {
const virtualRoom = MatrixClientPeg.get().getRoom(roomId); const virtualRoom = MatrixClientPeg.get().getRoom(roomId);
if (!virtualRoom) return null; if (!virtualRoom) return null;
const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE); const virtualRoomEvent = virtualRoom.getAccountData(VIRTUAL_ROOM_EVENT_TYPE);
@ -60,7 +60,7 @@ export default class VoipUserMapper {
return virtualRoomEvent.getContent()['native_room'] || null; return virtualRoomEvent.getContent()['native_room'] || null;
} }
public isVirtualRoom(room: Room):boolean { public isVirtualRoom(room: Room): boolean {
if (this.nativeRoomForVirtualRoom(room.roomId)) return true; if (this.nativeRoomForVirtualRoom(room.roomId)) return true;
if (this.virtualRoomIdCache.has(room.roomId)) return true; if (this.virtualRoomIdCache.has(room.roomId)) return true;
@ -79,6 +79,8 @@ export default class VoipUserMapper {
} }
public async onNewInvitedRoom(invitedRoom: Room) { public async onNewInvitedRoom(invitedRoom: Room) {
if (!CallHandler.sharedInstance().getSupportsVirtualRooms()) return;
const inviterId = invitedRoom.getDMInviter(); const inviterId = invitedRoom.getDMInviter();
console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`); console.log(`Checking virtual-ness of room ID ${invitedRoom.roomId}, invited by ${inviterId}`);
const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId); const result = await CallHandler.sharedInstance().sipNativeLookup(inviterId);

View file

@ -22,6 +22,7 @@ import classNames from "classnames";
import {Key} from "../../Keyboard"; import {Key} from "../../Keyboard";
import {Writeable} from "../../@types/common"; import {Writeable} from "../../@types/common";
import {replaceableComponent} from "../../utils/replaceableComponent";
// Shamelessly ripped off Modal.js. There's probably a better way // Shamelessly ripped off Modal.js. There's probably a better way
// of doing reusable widgets like dialog boxes & menus where we go and // of doing reusable widgets like dialog boxes & menus where we go and
@ -91,6 +92,7 @@ interface IState {
// Generic ContextMenu Portal wrapper // Generic ContextMenu Portal wrapper
// all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1} // all options inside the menu should be of role=menuitem/menuitemcheckbox/menuitemradiobutton and have tabIndex={-1}
// this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines. // this will allow the ContextMenu to manage its own focus using arrow keys as per the ARIA guidelines.
@replaceableComponent("structures.ContextMenu")
export class ContextMenu extends React.PureComponent<IProps, IState> { export class ContextMenu extends React.PureComponent<IProps, IState> {
private initialFocus: HTMLElement; private initialFocus: HTMLElement;
@ -467,6 +469,7 @@ export const useContextMenu = <T extends any = HTMLElement>(): ContextMenuTuple<
return [isOpen, button, open, close, setIsOpen]; return [isOpen, button, open, close, setIsOpen];
}; };
@replaceableComponent("structures.LegacyContextMenu")
export default class LegacyContextMenu extends ContextMenu { export default class LegacyContextMenu extends ContextMenu {
render() { render() {
return this.renderMenu(false); return this.renderMenu(false);

View file

@ -21,7 +21,9 @@ import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import classNames from 'classnames'; import classNames from 'classnames';
import * as FormattingUtils from '../../utils/FormattingUtils'; import * as FormattingUtils from '../../utils/FormattingUtils';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.CustomRoomTagPanel")
class CustomRoomTagPanel extends React.Component { class CustomRoomTagPanel extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -16,8 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import request from 'browser-request'; import request from 'browser-request';

View file

@ -26,10 +26,12 @@ import { _t } from '../../languageHandler';
import BaseCard from "../views/right_panel/BaseCard"; import BaseCard from "../views/right_panel/BaseCard";
import {RightPanelPhases} from "../../stores/RightPanelStorePhases"; import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice"; import DesktopBuildsNotice, {WarningKind} from "../views/elements/DesktopBuildsNotice";
import {replaceableComponent} from "../../utils/replaceableComponent";
/* /*
* Component which shows the filtered file using a TimelinePanel * Component which shows the filtered file using a TimelinePanel
*/ */
@replaceableComponent("structures.FilePanel")
class FilePanel extends React.Component { class FilePanel extends React.Component {
static propTypes = { static propTypes = {
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,

View file

@ -16,7 +16,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.GenericErrorPage")
export default class GenericErrorPage extends React.PureComponent { export default class GenericErrorPage extends React.PureComponent {
static propTypes = { static propTypes = {
title: PropTypes.object.isRequired, // jsx for title title: PropTypes.object.isRequired, // jsx for title

View file

@ -30,7 +30,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import UserTagTile from "../views/elements/UserTagTile"; import UserTagTile from "../views/elements/UserTagTile";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.GroupFilterPanel")
class GroupFilterPanel extends React.Component { class GroupFilterPanel extends React.Component {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;

View file

@ -39,6 +39,7 @@ import {Group} from "matrix-js-sdk";
import {allSettled, sleep} from "../../utils/promise"; import {allSettled, sleep} from "../../utils/promise";
import RightPanelStore from "../../stores/RightPanelStore"; import RightPanelStore from "../../stores/RightPanelStore";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
const LONG_DESC_PLACEHOLDER = _td( const LONG_DESC_PLACEHOLDER = _td(
`<h1>HTML for your community's page</h1> `<h1>HTML for your community's page</h1>
@ -391,6 +392,7 @@ class FeaturedUser extends React.Component {
const GROUP_JOINPOLICY_OPEN = "open"; const GROUP_JOINPOLICY_OPEN = "open";
const GROUP_JOINPOLICY_INVITE = "invite"; const GROUP_JOINPOLICY_INVITE = "invite";
@replaceableComponent("structures.GroupView")
export default class GroupView extends React.Component { export default class GroupView extends React.Component {
static propTypes = { static propTypes = {
groupId: PropTypes.string.isRequired, groupId: PropTypes.string.isRequired,

View file

@ -22,11 +22,13 @@ import {
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { HostSignupStore } from "../../stores/HostSignupStore"; import { HostSignupStore } from "../../stores/HostSignupStore";
import SdkConfig from "../../SdkConfig"; import SdkConfig from "../../SdkConfig";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {} interface IProps {}
interface IState {} interface IState {}
@replaceableComponent("structures.HostSignupAction")
export default class HostSignupAction extends React.PureComponent<IProps, IState> { export default class HostSignupAction extends React.PureComponent<IProps, IState> {
private openDialog = async () => { private openDialog = async () => {
await HostSignupStore.instance.setHostSignupActive(true); await HostSignupStore.instance.setHostSignupActive(true);

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.IndicatorScrollbar")
export default class IndicatorScrollbar extends React.Component { export default class IndicatorScrollbar extends React.Component {
static propTypes = { static propTypes = {
// If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator // If true, the scrollbar will append mx_IndicatorScrollbar_leftOverflowIndicator

View file

@ -22,9 +22,11 @@ import PropTypes from 'prop-types';
import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents'; import getEntryComponentForLoginType from '../views/auth/InteractiveAuthEntryComponents';
import * as sdk from '../../index'; import * as sdk from '../../index';
import {replaceableComponent} from "../../utils/replaceableComponent";
export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); export const ERROR_USER_CANCELLED = new Error("User cancelled auth session");
@replaceableComponent("structures.InteractiveAuthComponent")
export default class InteractiveAuthComponent extends React.Component { export default class InteractiveAuthComponent extends React.Component {
static propTypes = { static propTypes = {
// matrix client to use for UI auth requests // matrix client to use for UI auth requests

View file

@ -40,6 +40,7 @@ import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomListNumResults from "../views/rooms/RoomListNumResults"; import RoomListNumResults from "../views/rooms/RoomListNumResults";
import LeftPanelWidget from "./LeftPanelWidget"; import LeftPanelWidget from "./LeftPanelWidget";
import SpacePanel from "../views/spaces/SpacePanel"; import SpacePanel from "../views/spaces/SpacePanel";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -60,6 +61,7 @@ const cssClasses = [
"mx_RoomSublist_showNButton", "mx_RoomSublist_showNButton",
]; ];
@replaceableComponent("structures.LeftPanel")
export default class LeftPanel extends React.Component<IProps, IState> { export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef(); private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private groupFilterPanelWatcherRef: string; private groupFilterPanelWatcherRef: string;

View file

@ -57,6 +57,7 @@ import { ICollapseConfig } from "../../resizer/distributors/collapse";
import HostSignupContainer from '../views/host_signup/HostSignupContainer'; import HostSignupContainer from '../views/host_signup/HostSignupContainer';
import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBindingsManager'; import { getKeyBindingsManager, NavigationAction, RoomAction } from '../../KeyBindingsManager';
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
// We need to fetch each pinned message individually (if we don't already have it) // We need to fetch each pinned message individually (if we don't already have it)
// so each pinned message may trigger a request. Limit the number per room for sanity. // so each pinned message may trigger a request. Limit the number per room for sanity.
@ -97,8 +98,10 @@ interface IProps {
} }
interface IUsageLimit { interface IUsageLimit {
// "hs_disabled" is NOT a specced string, but is used in Synapse
// This is tracked over at https://github.com/matrix-org/synapse/issues/9237
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
limit_type: "monthly_active_user" | string; limit_type: "monthly_active_user" | "hs_disabled" | string;
// eslint-disable-next-line camelcase // eslint-disable-next-line camelcase
admin_contact?: string; admin_contact?: string;
} }
@ -106,6 +109,8 @@ interface IUsageLimit {
interface IState { interface IState {
syncErrorData?: { syncErrorData?: {
error: { error: {
// This is not specced, but used in Synapse. See
// https://github.com/matrix-org/synapse/issues/9237#issuecomment-768238922
data: IUsageLimit; data: IUsageLimit;
errcode: string; errcode: string;
}; };
@ -125,6 +130,7 @@ interface IState {
* *
* Components mounted below us can access the matrix client via the react context. * Components mounted below us can access the matrix client via the react context.
*/ */
@replaceableComponent("structures.LoggedInView")
class LoggedInView extends React.Component<IProps, IState> { class LoggedInView extends React.Component<IProps, IState> {
static displayName = 'LoggedInView'; static displayName = 'LoggedInView';

View file

@ -17,7 +17,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { Resizable } from 're-resizable'; import { Resizable } from 're-resizable';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.MainSplit")
export default class MainSplit extends React.Component { export default class MainSplit extends React.Component {
_onResizeStart = () => { _onResizeStart = () => {
this.props.resizeNotifier.startResizing(); this.props.resizeNotifier.startResizing();

View file

@ -84,6 +84,7 @@ import DialPadModal from "../views/voip/DialPadModal";
import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast'; import { showToast as showMobileGuideToast } from '../../toasts/MobileGuideToast';
import SpaceStore from "../../stores/SpaceStore"; import SpaceStore from "../../stores/SpaceStore";
import SpaceRoomDirectory from "./SpaceRoomDirectory"; import SpaceRoomDirectory from "./SpaceRoomDirectory";
import {replaceableComponent} from "../../utils/replaceableComponent";
/** constants for MatrixChat.state.view */ /** constants for MatrixChat.state.view */
export enum Views { export enum Views {
@ -208,6 +209,7 @@ interface IState {
roomJustCreatedOpts?: IOpts; roomJustCreatedOpts?: IOpts;
} }
@replaceableComponent("structures.MatrixChat")
export default class MatrixChat extends React.PureComponent<IProps, IState> { export default class MatrixChat extends React.PureComponent<IProps, IState> {
static displayName = "MatrixChat"; static displayName = "MatrixChat";

View file

@ -34,6 +34,7 @@ import {textForEvent} from "../../TextForEvent";
import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer"; import IRCTimelineProfileResizer from "../views/elements/IRCTimelineProfileResizer";
import DMRoomMap from "../../utils/DMRoomMap"; import DMRoomMap from "../../utils/DMRoomMap";
import NewRoomIntro from "../views/rooms/NewRoomIntro"; import NewRoomIntro from "../views/rooms/NewRoomIntro";
import {replaceableComponent} from "../../utils/replaceableComponent";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = ['m.sticker', 'm.room.message']; const continuedTypes = ['m.sticker', 'm.room.message'];
@ -66,6 +67,7 @@ const isMembershipChange = (e) => e.getType() === 'm.room.member' || e.getType()
/* (almost) stateless UI component which builds the event tiles in the room timeline. /* (almost) stateless UI component which builds the event tiles in the room timeline.
*/ */
@replaceableComponent("structures.MessagePanel")
export default class MessagePanel extends React.Component { export default class MessagePanel extends React.Component {
static propTypes = { static propTypes = {
// true to give the component a 'display: none' style. // true to give the component a 'display: none' style.
@ -498,6 +500,9 @@ export default class MessagePanel extends React.Component {
let prevEvent = null; // the last event we showed let prevEvent = null; // the last event we showed
// Note: the EventTile might still render a "sent/sending receipt" independent of
// this information. When not providing read receipt information, the tile is likely
// to assume that sent receipts are to be shown more often.
this._readReceiptsByEvent = {}; this._readReceiptsByEvent = {};
if (this.props.showReadReceipts) { if (this.props.showReadReceipts) {
this._readReceiptsByEvent = this._getReadReceiptsByShownEvent(); this._readReceiptsByEvent = this._getReadReceiptsByShownEvent();
@ -534,10 +539,17 @@ export default class MessagePanel extends React.Component {
const nextEvent = i < this.props.events.length - 1 const nextEvent = i < this.props.events.length - 1
? this.props.events[i + 1] ? this.props.events[i + 1]
: null; : null;
// The next event with tile is used to to determine the 'last successful' flag
// when rendering the tile. The shouldShowEvent function is pretty quick at what
// it does, so this should have no significant cost even when a room is used for
// not-chat purposes.
const nextTile = this.props.events.slice(i + 1).find(e => this._shouldShowEvent(e));
// make sure we unpack the array returned by _getTilesForEvent, // make sure we unpack the array returned by _getTilesForEvent,
// otherwise react will auto-generate keys and we will end up // otherwise react will auto-generate keys and we will end up
// replacing all of the DOM elements every time we paginate. // replacing all of the DOM elements every time we paginate.
ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent)); ret.push(...this._getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextTile));
prevEvent = mxEv; prevEvent = mxEv;
} }
@ -553,7 +565,7 @@ export default class MessagePanel extends React.Component {
return ret; return ret;
} }
_getTilesForEvent(prevEvent, mxEv, last, nextEvent) { _getTilesForEvent(prevEvent, mxEv, last, nextEvent, nextEventWithTile) {
const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary'); const TileErrorBoundary = sdk.getComponent('messages.TileErrorBoundary');
const EventTile = sdk.getComponent('rooms.EventTile'); const EventTile = sdk.getComponent('rooms.EventTile');
const DateSeparator = sdk.getComponent('messages.DateSeparator'); const DateSeparator = sdk.getComponent('messages.DateSeparator');
@ -595,6 +607,30 @@ export default class MessagePanel extends React.Component {
const readReceipts = this._readReceiptsByEvent[eventId]; const readReceipts = this._readReceiptsByEvent[eventId];
let isLastSuccessful = false;
const isSentState = s => !s || s === 'sent';
const isSent = isSentState(mxEv.getAssociatedStatus());
const hasNextEvent = nextEvent && this._shouldShowEvent(nextEvent);
if (!hasNextEvent && isSent) {
isLastSuccessful = true;
} else if (hasNextEvent && isSent && !isSentState(nextEvent.getAssociatedStatus())) {
isLastSuccessful = true;
}
// This is a bit nuanced, but if our next event is hidden but a future event is not
// hidden then we're not the last successful.
if (
nextEventWithTile &&
nextEventWithTile !== nextEvent &&
isSentState(nextEventWithTile.getAssociatedStatus())
) {
isLastSuccessful = false;
}
// We only want to consider "last successful" if the event is sent by us, otherwise of course
// it's successful: we received it.
isLastSuccessful = isLastSuccessful && mxEv.getSender() === MatrixClientPeg.get().getUserId();
// use txnId as key if available so that we don't remount during sending // use txnId as key if available so that we don't remount during sending
ret.push( ret.push(
<li <li
@ -620,6 +656,7 @@ export default class MessagePanel extends React.Component {
permalinkCreator={this.props.permalinkCreator} permalinkCreator={this.props.permalinkCreator}
last={last} last={last}
lastInSection={willWantDateSeparator} lastInSection={willWantDateSeparator}
lastSuccessful={isLastSuccessful}
isSelectedEvent={highlight} isSelectedEvent={highlight}
getRelationsForEvent={this.props.getRelationsForEvent} getRelationsForEvent={this.props.getRelationsForEvent}
showReactions={this.props.showReactions} showReactions={this.props.showReactions}

View file

@ -24,7 +24,9 @@ import dis from '../../dispatcher/dispatcher';
import AccessibleButton from '../views/elements/AccessibleButton'; import AccessibleButton from '../views/elements/AccessibleButton';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.MyGroups")
export default class MyGroups extends React.Component { export default class MyGroups extends React.Component {
static contextType = MatrixClientContext; static contextType = MatrixClientContext;

View file

@ -18,6 +18,7 @@ import * as React from "react";
import { ComponentClass } from "../../@types/common"; import { ComponentClass } from "../../@types/common";
import NonUrgentToastStore from "../../stores/NonUrgentToastStore"; import NonUrgentToastStore from "../../stores/NonUrgentToastStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps { interface IProps {
} }
@ -26,6 +27,7 @@ interface IState {
toasts: ComponentClass[], toasts: ComponentClass[],
} }
@replaceableComponent("structures.NonUrgentToastContainer")
export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> { export default class NonUrgentToastContainer extends React.PureComponent<IProps, IState> {
public constructor(props, context) { public constructor(props, context) {
super(props, context); super(props, context);

View file

@ -23,10 +23,12 @@ import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg"; import {MatrixClientPeg} from "../../MatrixClientPeg";
import * as sdk from "../../index"; import * as sdk from "../../index";
import BaseCard from "../views/right_panel/BaseCard"; import BaseCard from "../views/right_panel/BaseCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
/* /*
* Component which shows the global notification list using a TimelinePanel * Component which shows the global notification list using a TimelinePanel
*/ */
@replaceableComponent("structures.NotificationPanel")
class NotificationPanel extends React.Component { class NotificationPanel extends React.Component {
static propTypes = { static propTypes = {
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,

View file

@ -34,7 +34,9 @@ import MatrixClientContext from "../../contexts/MatrixClientContext";
import {Action} from "../../dispatcher/actions"; import {Action} from "../../dispatcher/actions";
import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard";
import WidgetCard from "../views/right_panel/WidgetCard"; import WidgetCard from "../views/right_panel/WidgetCard";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.RightPanel")
export default class RightPanel extends React.Component { export default class RightPanel extends React.Component {
static get propTypes() { static get propTypes() {
return { return {

View file

@ -34,6 +34,7 @@ import GroupFilterOrderStore from "../../stores/GroupFilterOrderStore";
import GroupStore from "../../stores/GroupStore"; import GroupStore from "../../stores/GroupStore";
import FlairStore from "../../stores/FlairStore"; import FlairStore from "../../stores/FlairStore";
import CountlyAnalytics from "../../CountlyAnalytics"; import CountlyAnalytics from "../../CountlyAnalytics";
import {replaceableComponent} from "../../utils/replaceableComponent";
const MAX_NAME_LENGTH = 80; const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800; const MAX_TOPIC_LENGTH = 800;
@ -42,6 +43,7 @@ function track(action) {
Analytics.trackEvent('RoomDirectory', action); Analytics.trackEvent('RoomDirectory', action);
} }
@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component { export default class RoomDirectory extends React.Component {
static propTypes = { static propTypes = {
initialText: PropTypes.string, initialText: PropTypes.string,

View file

@ -25,6 +25,7 @@ import { Action } from "../../dispatcher/actions";
import RoomListStore from "../../stores/room-list/RoomListStore"; import RoomListStore from "../../stores/room-list/RoomListStore";
import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition"; import { NameFilterCondition } from "../../stores/room-list/filters/NameFilterCondition";
import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager"; import { getKeyBindingsManager, RoomListAction } from "../../KeyBindingsManager";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -37,6 +38,7 @@ interface IState {
focused: boolean; focused: boolean;
} }
@replaceableComponent("structures.RoomSearch")
export default class RoomSearch extends React.PureComponent<IProps, IState> { export default class RoomSearch extends React.PureComponent<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private inputRef: React.RefObject<HTMLInputElement> = createRef(); private inputRef: React.RefObject<HTMLInputElement> = createRef();

View file

@ -23,6 +23,7 @@ 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 {replaceableComponent} from "../../utils/replaceableComponent";
const STATUS_BAR_HIDDEN = 0; const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1; const STATUS_BAR_EXPANDED = 1;
@ -35,6 +36,7 @@ function getUnsentMessages(room) {
}); });
} }
@replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.Component { export default class RoomStatusBar extends React.Component {
static propTypes = { static propTypes = {
// the room this statusbar is representing. // the room this statusbar is representing.
@ -195,6 +197,10 @@ export default class RoomStatusBar extends React.Component {
"Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " + "Your message wasn't sent because this homeserver has hit its Monthly Active User Limit. " +
"Please <a>contact your service administrator</a> to continue using the service.", "Please <a>contact your service administrator</a> to continue using the service.",
), ),
'hs_disabled': _td(
"Your message wasn't sent because this homeserver has been blocked by it's administrator. " +
"Please <a>contact your service administrator</a> to continue using the service.",
),
'': _td( '': _td(
"Your message wasn't sent because this homeserver has exceeded a resource limit. " + "Your message wasn't sent because this homeserver has exceeded a resource limit. " +
"Please <a>contact your service administrator</a> to continue using the service.", "Please <a>contact your service administrator</a> to continue using the service.",

View file

@ -82,6 +82,7 @@ import { getKeyBindingsManager, RoomAction } from '../../KeyBindingsManager';
import { objectHasDiff } from "../../utils/objects"; import { objectHasDiff } from "../../utils/objects";
import SpaceRoomView from "./SpaceRoomView"; import SpaceRoomView from "./SpaceRoomView";
import { IOpts } from "../../createRoom"; import { IOpts } from "../../createRoom";
import {replaceableComponent} from "../../utils/replaceableComponent";
const DEBUG = false; const DEBUG = false;
let debuglog = function(msg: string) {}; let debuglog = function(msg: string) {};
@ -192,8 +193,10 @@ export interface IState {
rejecting?: boolean; rejecting?: boolean;
rejectError?: Error; rejectError?: Error;
hasPinnedWidgets?: boolean; hasPinnedWidgets?: boolean;
dragCounter: number;
} }
@replaceableComponent("structures.RoomView")
export default class RoomView extends React.Component<IProps, IState> { export default class RoomView extends React.Component<IProps, IState> {
private readonly dispatcherRef: string; private readonly dispatcherRef: string;
private readonly roomStoreToken: EventSubscription; private readonly roomStoreToken: EventSubscription;
@ -242,6 +245,7 @@ export default class RoomView extends React.Component<IProps, IState> {
canReply: false, canReply: false,
layout: SettingsStore.getValue("layout"), layout: SettingsStore.getValue("layout"),
matrixClientIsReady: this.context && this.context.isInitialSyncComplete(), matrixClientIsReady: this.context && this.context.isInitialSyncComplete(),
dragCounter: 0,
}; };
this.dispatcherRef = dis.register(this.onAction); this.dispatcherRef = dis.register(this.onAction);
@ -535,8 +539,8 @@ export default class RoomView extends React.Component<IProps, IState> {
if (!roomView.ondrop) { if (!roomView.ondrop) {
roomView.addEventListener('drop', this.onDrop); roomView.addEventListener('drop', this.onDrop);
roomView.addEventListener('dragover', this.onDragOver); roomView.addEventListener('dragover', this.onDragOver);
roomView.addEventListener('dragleave', this.onDragLeaveOrEnd); roomView.addEventListener('dragenter', this.onDragEnter);
roomView.addEventListener('dragend', this.onDragLeaveOrEnd); roomView.addEventListener('dragleave', this.onDragLeave);
} }
} }
@ -580,8 +584,8 @@ export default class RoomView extends React.Component<IProps, IState> {
const roomView = this.roomView.current; const roomView = this.roomView.current;
roomView.removeEventListener('drop', this.onDrop); roomView.removeEventListener('drop', this.onDrop);
roomView.removeEventListener('dragover', this.onDragOver); roomView.removeEventListener('dragover', this.onDragOver);
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd); roomView.removeEventListener('dragenter', this.onDragEnter);
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd); roomView.removeEventListener('dragleave', this.onDragLeave);
} }
dis.unregister(this.dispatcherRef); dis.unregister(this.dispatcherRef);
if (this.context) { if (this.context) {
@ -703,9 +707,9 @@ export default class RoomView extends React.Component<IProps, IState> {
[payload.file], this.state.room.roomId, this.context); [payload.file], this.state.room.roomId, this.context);
break; break;
case 'notifier_enabled': case 'notifier_enabled':
case 'upload_started': case Action.UploadStarted:
case 'upload_finished': case Action.UploadFinished:
case 'upload_canceled': case Action.UploadCanceled:
this.forceUpdate(); this.forceUpdate();
break; break;
case 'call_state': { case 'call_state': {
@ -1135,6 +1139,31 @@ export default class RoomView extends React.Component<IProps, IState> {
this.updateTopUnreadMessagesBar(); this.updateTopUnreadMessagesBar();
}; };
private onDragEnter = ev => {
ev.stopPropagation();
ev.preventDefault();
this.setState({
dragCounter: this.state.dragCounter + 1,
draggingFile: true,
});
};
private onDragLeave = ev => {
ev.stopPropagation();
ev.preventDefault();
this.setState({
dragCounter: this.state.dragCounter - 1,
});
if (this.state.dragCounter === 0) {
this.setState({
draggingFile: false,
});
}
};
private onDragOver = ev => { private onDragOver = ev => {
ev.stopPropagation(); ev.stopPropagation();
ev.preventDefault(); ev.preventDefault();
@ -1142,7 +1171,6 @@ export default class RoomView extends React.Component<IProps, IState> {
ev.dataTransfer.dropEffect = 'none'; ev.dataTransfer.dropEffect = 'none';
if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) { if (ev.dataTransfer.types.includes("Files") || ev.dataTransfer.types.includes("application/x-moz-file")) {
this.setState({ draggingFile: true });
ev.dataTransfer.dropEffect = 'copy'; ev.dataTransfer.dropEffect = 'copy';
} }
}; };
@ -1153,14 +1181,12 @@ export default class RoomView extends React.Component<IProps, IState> {
ContentMessages.sharedInstance().sendContentListToRoom( ContentMessages.sharedInstance().sendContentListToRoom(
ev.dataTransfer.files, this.state.room.roomId, this.context, ev.dataTransfer.files, this.state.room.roomId, this.context,
); );
this.setState({ draggingFile: false });
dis.fire(Action.FocusComposer); dis.fire(Action.FocusComposer);
};
private onDragLeaveOrEnd = ev => { this.setState({
ev.stopPropagation(); draggingFile: false,
ev.preventDefault(); dragCounter: this.state.dragCounter - 1,
this.setState({ draggingFile: false }); });
}; };
private injectSticker(url, info, text) { private injectSticker(url, info, text) {
@ -1762,6 +1788,19 @@ export default class RoomView extends React.Component<IProps, IState> {
} }
} }
let fileDropTarget = null;
if (this.state.draggingFile) {
fileDropTarget = (
<div className="mx_RoomView_fileDropTarget">
<img
src={require("../../../res/img/upload-big.svg")}
className="mx_RoomView_fileDropTarget_image"
/>
{ _t("Drop file here to upload") }
</div>
);
}
// We have successfully loaded this room, and are not previewing. // We have successfully loaded this room, and are not previewing.
// Display the "normal" room view. // Display the "normal" room view.
@ -1868,7 +1907,7 @@ export default class RoomView extends React.Component<IProps, IState> {
); );
} }
if (this.state.room?.isSpaceRoom()) { if (SettingsStore.getValue("feature_spaces") && this.state.room?.isSpaceRoom()) {
return <SpaceRoomView return <SpaceRoomView
space={this.state.room} space={this.state.room}
justCreatedOpts={this.props.justCreatedOpts} justCreatedOpts={this.props.justCreatedOpts}
@ -1885,7 +1924,6 @@ export default class RoomView extends React.Component<IProps, IState> {
room={this.state.room} room={this.state.room}
fullHeight={false} fullHeight={false}
userId={this.context.credentials.userId} userId={this.context.credentials.userId}
draggingFile={this.state.draggingFile}
maxHeight={this.state.auxPanelMaxHeight} maxHeight={this.state.auxPanelMaxHeight}
showApps={this.state.showApps} showApps={this.state.showApps}
onResize={this.onResize} onResize={this.onResize}
@ -2054,6 +2092,7 @@ export default class RoomView extends React.Component<IProps, IState> {
<div className="mx_RoomView_body"> <div className="mx_RoomView_body">
{auxPanel} {auxPanel}
<div className={timelineClasses}> <div className={timelineClasses}>
{fileDropTarget}
{topUnreadMessagesBar} {topUnreadMessagesBar}
{jumpToBottom} {jumpToBottom}
{messagePanel} {messagePanel}

View file

@ -19,6 +19,7 @@ import PropTypes from 'prop-types';
import { Key } from '../../Keyboard'; import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import {replaceableComponent} from "../../utils/replaceableComponent";
const DEBUG_SCROLL = false; const DEBUG_SCROLL = false;
@ -83,6 +84,7 @@ if (DEBUG_SCROLL) {
* offset as normal. * offset as normal.
*/ */
@replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component { export default class ScrollPanel extends React.Component {
static propTypes = { static propTypes = {
/* stickyBottom: if set to true, then once the user hits the bottom of /* stickyBottom: if set to true, then once the user hits the bottom of

View file

@ -22,7 +22,9 @@ import dis from '../../dispatcher/dispatcher';
import {throttle} from 'lodash'; import {throttle} from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton'; import AccessibleButton from '../../components/views/elements/AccessibleButton';
import classNames from 'classnames'; import classNames from 'classnames';
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.SearchBox")
export default class SearchBox extends React.Component { export default class SearchBox extends React.Component {
static propTypes = { static propTypes = {
onSearch: PropTypes.func, onSearch: PropTypes.func,

View file

@ -64,6 +64,7 @@ export interface ISpaceSummaryEvent {
state_key: string; state_key: string;
content: { content: {
order?: string; order?: string;
suggested?: boolean;
auto_join?: boolean; auto_join?: boolean;
via?: string; via?: string;
}; };
@ -91,7 +92,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space"); const name = space.name || space.canonical_alias || space.aliases?.[0] || _t("Unnamed Space");
const evContent = event?.getContent(); const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via); const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -102,12 +103,12 @@ const SubSpace: React.FC<ISubspaceProps> = ({
let actions; let actions;
if (editing && queueAction) { if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => { const setSuggested = () => {
_setAutoJoin(v => { _setSuggested(v => {
queueAction({ queueAction({
event, event,
removed, removed,
autoJoin: !v, suggested: !v,
}); });
return !v; return !v;
}); });
@ -118,7 +119,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
queueAction({ queueAction({
event, event,
removed: !v, removed: !v,
autoJoin, suggested,
}); });
return !v; return !v;
}); });
@ -131,7 +132,7 @@ const SubSpace: React.FC<ISubspaceProps> = ({
} else { } else {
actions = <React.Fragment> actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} /> <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} /> <StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>; </React.Fragment>;
} }
} else { } else {
@ -182,8 +183,8 @@ const SubSpace: React.FC<ISubspaceProps> = ({
interface IAction { interface IAction {
event: MatrixEvent; event: MatrixEvent;
suggested: boolean;
removed: boolean; removed: boolean;
autoJoin: boolean;
} }
interface IRoomTileProps { interface IRoomTileProps {
@ -199,7 +200,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room"); const name = room.name || room.canonical_alias || room.aliases?.[0] || _t("Unnamed Room");
const evContent = event?.getContent(); const evContent = event?.getContent();
const [autoJoin, _setAutoJoin] = useState(evContent?.auto_join); const [suggested, _setSuggested] = useState(evContent?.suggested);
const [removed, _setRemoved] = useState(!evContent?.via); const [removed, _setRemoved] = useState(!evContent?.via);
const cli = MatrixClientPeg.get(); const cli = MatrixClientPeg.get();
@ -209,12 +210,12 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
let actions; let actions;
if (editing && queueAction) { if (editing && queueAction) {
if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) { if (event && cli.getRoom(event.getRoomId())?.currentState.maySendStateEvent(event.getType(), cli.getUserId())) {
const setAutoJoin = () => { const setSuggested = () => {
_setAutoJoin(v => { _setSuggested(v => {
queueAction({ queueAction({
event, event,
removed, removed,
autoJoin: !v, suggested: !v,
}); });
return !v; return !v;
}); });
@ -225,7 +226,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
queueAction({ queueAction({
event, event,
removed: !v, removed: !v,
autoJoin, suggested,
}); });
return !v; return !v;
}); });
@ -238,7 +239,7 @@ const RoomTile = ({ room, event, editing, queueAction, onPreviewClick, onJoinCli
} else { } else {
actions = <React.Fragment> actions = <React.Fragment>
<FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} /> <FormButton kind="danger" onClick={setRemoved} label={_t("Remove from Space")} />
<StyledCheckbox checked={autoJoin} onChange={setAutoJoin} /> <StyledCheckbox checked={suggested} onChange={setSuggested} />
</React.Fragment>; </React.Fragment>;
} }
} else { } else {
@ -445,10 +446,10 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
const onSaveButtonClicked = () => { const onSaveButtonClicked = () => {
// TODO setBusy // TODO setBusy
pendingActions.current.forEach(({event, autoJoin, removed}) => { pendingActions.current.forEach(({event, suggested, removed}) => {
const content = { const content = {
...event.getContent(), ...event.getContent(),
auto_join: autoJoin, suggested,
}; };
if (removed) { if (removed) {
@ -463,7 +464,7 @@ const SpaceRoomDirectory: React.FC<IProps> = ({ space, initialText = "", onFinis
if (isEditing) { if (isEditing) {
adminButton = <React.Fragment> adminButton = <React.Fragment>
<FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} /> <FormButton label={_t("Save changes")} onClick={onSaveButtonClicked} />
<span>{ _t("All users join by default") }</span> <span>{ _t("Promoted to users") }</span>
</React.Fragment>; </React.Fragment>;
} else { } else {
adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />; adminButton = <FormButton label={_t("Manage rooms")} onClick={onManageButtonClicked} />;

View file

@ -94,26 +94,95 @@ const useMyRoomMembership = (room: Room) => {
return membership; return membership;
}; };
const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => { const SpacePreview = ({ space, onJoinButtonClicked, onRejectButtonClicked }) => {
const cli = useContext(MatrixClientContext); const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space); const myMembership = useMyRoomMembership(space);
const joinRule = space.getJoinRule();
const userId = cli.getUserId();
let inviterSection;
let joinButtons; let joinButtons;
if (myMembership === "invite") { if (myMembership === "invite") {
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons"> const inviteSender = space.getMember(cli.getUserId())?.events.member?.getSender();
<FormButton label={_t("Accept Invite")} onClick={onJoinButtonClicked} /> const inviter = inviteSender && space.getMember(inviteSender);
<AccessibleButton kind="link" onClick={onRejectButtonClicked}>
{_t("Decline")} if (inviteSender) {
</AccessibleButton> inviterSection = <div className="mx_SpaceRoomView_preview_inviter">
</div>; <MemberAvatar member={inviter} width={32} height={32} />
} else if (myMembership !== "join" && joinRule === "public") { <div>
joinButtons = <div className="mx_SpaceRoomView_landing_joinButtons"> <div className="mx_SpaceRoomView_preview_inviter_name">
<FormButton label={_t("Join")} onClick={onJoinButtonClicked} /> { _t("<inviter/> invites you", {}, {
</div>; inviter: () => <b>{ inviter.name || inviteSender }</b>,
}) }
</div>
{ inviter ? <div className="mx_SpaceRoomView_preview_inviter_mxid">
{ inviteSender }
</div> : null }
</div>
</div>;
}
joinButtons = <>
<FormButton label={_t("Reject")} kind="secondary" onClick={onRejectButtonClicked} />
<FormButton label={_t("Accept")} onClick={onJoinButtonClicked} />
</>;
} else {
joinButtons = <FormButton label={_t("Join")} onClick={onJoinButtonClicked} />
} }
let visibilitySection;
if (space.getJoinRule() === "public") {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_public">
{ _t("Public space") }
</span>;
} else {
visibilitySection = <span className="mx_SpaceRoomView_preview_info_private">
{ _t("Private space") }
</span>;
}
return <div className="mx_SpaceRoomView_preview">
{ inviterSection }
<RoomAvatar room={space} height={80} width={80} viewAvatarOnClick={true} />
<h1 className="mx_SpaceRoomView_preview_name">
<RoomName room={space} />
</h1>
<div className="mx_SpaceRoomView_preview_info">
{ visibilitySection }
<RoomMemberCount room={space}>
{(count) => count > 0 ? (
<AccessibleButton
className="mx_SpaceRoomView_preview_memberCount"
kind="link"
onClick={() => {
defaultDispatcher.dispatch<SetRightPanelPhasePayload>({
action: Action.SetRightPanelPhase,
phase: RightPanelPhases.RoomMemberList,
refireParams: { space },
});
}}
>
{ _t("%(count)s members", { count }) }
</AccessibleButton>
) : null}
</RoomMemberCount>
</div>
<RoomTopic room={space}>
{(topic, ref) =>
<div className="mx_SpaceRoomView_preview_topic" ref={ref}>
{ topic }
</div>
}
</RoomTopic>
<div className="mx_SpaceRoomView_preview_joinButtons">
{ joinButtons }
</div>
</div>;
};
const SpaceLanding = ({ space }) => {
const cli = useContext(MatrixClientContext);
const myMembership = useMyRoomMembership(space);
const userId = cli.getUserId();
let inviteButton; let inviteButton;
if (myMembership === "join" && space.canInvite(userId)) { if (myMembership === "join" && space.canInvite(userId)) {
inviteButton = ( inviteButton = (
@ -227,26 +296,7 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
) : null} ) : null}
</RoomMemberCount> </RoomMemberCount>
</div> }; </div> };
if (myMembership === "invite") { if (shouldShowSpaceSettings(cli, space)) {
const inviteSender = space.getMember(userId)?.events.member?.getSender();
const inviter = inviteSender && space.getMember(inviteSender);
if (inviteSender) {
return _t("<inviter/> invited you to <name/>", {}, {
name: tags.name,
inviter: () => inviter
? <span className="mx_SpaceRoomView_landing_inviter">
<MemberAvatar member={inviter} width={26} height={26} viewUserOnClick={true} />
{ inviter.name }
</span>
: <span className="mx_SpaceRoomView_landing_inviter">
{ inviteSender }
</span>,
}) as JSX.Element;
} else {
return _t("You have been invited to <name/>", {}, tags) as JSX.Element;
}
} else if (shouldShowSpaceSettings(cli, space)) {
if (space.getJoinRule() === "public") { if (space.getJoinRule() === "public") {
return _t("Your public space <name/>", {}, tags) as JSX.Element; return _t("Your public space <name/>", {}, tags) as JSX.Element;
} else { } else {
@ -260,7 +310,6 @@ const SpaceLanding = ({ space, onJoinButtonClicked, onRejectButtonClicked }) =>
<div className="mx_SpaceRoomView_landing_topic"> <div className="mx_SpaceRoomView_landing_topic">
<RoomTopic room={space} /> <RoomTopic room={space} />
</div> </div>
{ joinButtons }
<div className="mx_SpaceRoomView_landing_adminButtons"> <div className="mx_SpaceRoomView_landing_adminButtons">
{ inviteButton } { inviteButton }
{ addRoomButtons } { addRoomButtons }
@ -548,16 +597,19 @@ export default class SpaceRoomView extends React.PureComponent<IProps, IState> {
private renderBody() { private renderBody() {
switch (this.state.phase) { switch (this.state.phase) {
case Phase.Landing: case Phase.Landing:
return <SpaceLanding if (this.props.space.getMyMembership() === "join") {
space={this.props.space} return <SpaceLanding space={this.props.space} />;
onJoinButtonClicked={this.props.onJoinButtonClicked} } else {
onRejectButtonClicked={this.props.onRejectButtonClicked} return <SpacePreview
/>; space={this.props.space}
onJoinButtonClicked={this.props.onJoinButtonClicked}
onRejectButtonClicked={this.props.onRejectButtonClicked}
/>;
}
case Phase.PublicCreateRooms: case Phase.PublicCreateRooms:
return <SpaceSetupFirstRooms return <SpaceSetupFirstRooms
space={this.props.space} space={this.props.space}
title={_t("What discussions do you want to have?")} title={_t("What are some things you want to discuss?")}
description={_t("We'll create rooms for each topic.")} description={_t("We'll create rooms for each topic.")}
onFinished={() => this.setState({ phase: Phase.PublicShare })} onFinished={() => this.setState({ phase: Phase.PublicShare })}
/>; />;

View file

@ -20,6 +20,7 @@ import * as React from "react";
import {_t} from '../../languageHandler'; import {_t} from '../../languageHandler';
import * as sdk from "../../index"; import * as sdk from "../../index";
import AutoHideScrollbar from './AutoHideScrollbar'; import AutoHideScrollbar from './AutoHideScrollbar';
import {replaceableComponent} from "../../utils/replaceableComponent";
/** /**
* Represents a tab for the TabbedView. * Represents a tab for the TabbedView.
@ -45,6 +46,7 @@ interface IState {
activeTabIndex: number; activeTabIndex: number;
} }
@replaceableComponent("structures.TabbedView")
export default class TabbedView extends React.Component<IProps, IState> { export default class TabbedView extends React.Component<IProps, IState> {
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);

View file

@ -37,6 +37,7 @@ import EditorStateTransfer from '../../utils/EditorStateTransfer';
import {haveTileForEvent} from "../views/rooms/EventTile"; import {haveTileForEvent} from "../views/rooms/EventTile";
import {UIFeature} from "../../settings/UIFeature"; import {UIFeature} from "../../settings/UIFeature";
import {objectHasDiff} from "../../utils/objects"; import {objectHasDiff} from "../../utils/objects";
import {replaceableComponent} from "../../utils/replaceableComponent";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -55,6 +56,7 @@ if (DEBUG) {
* *
* Also responsible for handling and sending read receipts. * Also responsible for handling and sending read receipts.
*/ */
@replaceableComponent("structures.TimelinePanel")
class TimelinePanel extends React.Component { class TimelinePanel extends React.Component {
static propTypes = { static propTypes = {
// The js-sdk EventTimelineSet object for the timeline sequence we are // The js-sdk EventTimelineSet object for the timeline sequence we are

View file

@ -17,12 +17,14 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import ToastStore, {IToast} from "../../stores/ToastStore"; import ToastStore, {IToast} from "../../stores/ToastStore";
import classNames from "classnames"; import classNames from "classnames";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IState { interface IState {
toasts: IToast<any>[]; toasts: IToast<any>[];
countSeen: number; countSeen: number;
} }
@replaceableComponent("structures.ToastContainer")
export default class ToastContainer extends React.Component<{}, IState> { export default class ToastContainer extends React.Component<{}, IState> {
constructor(props, context) { constructor(props, context) {
super(props, context); super(props, context);

View file

@ -1,109 +0,0 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';
export default class UploadBar extends React.Component {
static propTypes = {
room: PropTypes.object,
};
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
}
onAction = payload => {
switch (payload.action) {
case 'upload_progress':
case 'upload_finished':
case 'upload_canceled':
case 'upload_failed':
if (this.mounted) this.forceUpdate();
break;
}
};
render() {
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
// for testing UI... - also fix up the ContentMessages.getCurrentUploads().length
// check in RoomView
//
// uploads = [{
// roomId: this.props.room.roomId,
// loaded: 123493,
// total: 347534,
// fileName: "testing_fooble.jpg",
// }];
if (uploads.length == 0) {
return <div />;
}
let upload;
for (let i = 0; i < uploads.length; ++i) {
if (uploads[i].roomId == this.props.room.roomId) {
upload = uploads[i];
break;
}
}
if (!upload) {
return <div />;
}
const innerProgressStyle = {
width: ((upload.loaded / (upload.total || 1)) * 100) + '%',
};
let uploadedSize = filesize(upload.loaded);
const totalSize = filesize(upload.total);
if (uploadedSize.replace(/^.* /, '') === totalSize.replace(/^.* /, '')) {
uploadedSize = uploadedSize.replace(/ .*/, '');
}
// MUST use var name 'count' for pluralization to kick in
const uploadText = _t(
"Uploading %(filename)s and %(count)s others", {filename: upload.fileName, count: (uploads.length - 1)},
);
return (
<div className="mx_UploadBar">
<div className="mx_UploadBar_uploadProgressOuter">
<div className="mx_UploadBar_uploadProgressInner" style={innerProgressStyle}></div>
</div>
<img className="mx_UploadBar_uploadIcon mx_filterFlipColor" src={require("../../../res/img/fileicon.png")} width="17" height="22" />
<img className="mx_UploadBar_uploadCancel mx_filterFlipColor" src={require("../../../res/img/cancel.svg")} width="18" height="18"
onClick={function() { ContentMessages.sharedInstance().cancelUpload(upload.promise); }}
/>
<div className="mx_UploadBar_uploadBytes">
{ uploadedSize } / { totalSize }
</div>
<div className="mx_UploadBar_uploadFilename">{ uploadText }</div>
</div>
);
}
}

View file

@ -0,0 +1,102 @@
/*
Copyright 2015, 2016, 2019, 2021 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import { Room } from "matrix-js-sdk/src/models/room";
import ContentMessages from '../../ContentMessages';
import dis from "../../dispatcher/dispatcher";
import filesize from "filesize";
import { _t } from '../../languageHandler';
import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions";
import ProgressBar from "../views/elements/ProgressBar";
import AccessibleButton from "../views/elements/AccessibleButton";
import { IUpload } from "../../models/IUpload";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps {
room: Room;
}
interface IState {
currentUpload?: IUpload;
uploadsHere: IUpload[];
}
@replaceableComponent("structures.UploadBar")
export default class UploadBar extends React.Component<IProps, IState> {
private dispatcherRef: string;
private mounted: boolean;
constructor(props) {
super(props);
this.state = {uploadsHere: []};
}
componentDidMount() {
this.dispatcherRef = dis.register(this.onAction);
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
dis.unregister(this.dispatcherRef);
}
private onAction = (payload: ActionPayload) => {
switch (payload.action) {
case Action.UploadStarted:
case Action.UploadProgress:
case Action.UploadFinished:
case Action.UploadCanceled:
case Action.UploadFailed: {
if (!this.mounted) return;
const uploads = ContentMessages.sharedInstance().getCurrentUploads();
const uploadsHere = uploads.filter(u => u.roomId === this.props.room.roomId);
this.setState({currentUpload: uploadsHere[0], uploadsHere});
break;
}
}
};
private onCancelClick = (ev) => {
ev.preventDefault();
ContentMessages.sharedInstance().cancelUpload(this.state.currentUpload.promise);
};
render() {
if (!this.state.currentUpload) {
return null;
}
// MUST use var name 'count' for pluralization to kick in
const uploadText = _t(
"Uploading %(filename)s and %(count)s others", {
filename: this.state.currentUpload.fileName,
count: this.state.uploadsHere.length - 1,
},
);
const uploadSize = filesize(this.state.currentUpload.total);
return (
<div className="mx_UploadBar">
<div className="mx_UploadBar_filename">{uploadText} ({uploadSize})</div>
<AccessibleButton onClick={this.onCancelClick} className='mx_UploadBar_cancel' />
<ProgressBar value={this.state.currentUpload.loaded} max={this.state.currentUpload.total} />
</div>
);
}
}

View file

@ -56,6 +56,7 @@ import HostSignupAction from "./HostSignupAction";
import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes"; import { IHostSignupConfig } from "../views/dialogs/HostSignupDialogTypes";
import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore"; import SpaceStore, { UPDATE_SELECTED_SPACE } from "../../stores/SpaceStore";
import RoomName from "../views/elements/RoomName"; import RoomName from "../views/elements/RoomName";
import {replaceableComponent} from "../../utils/replaceableComponent";
interface IProps { interface IProps {
isMinimized: boolean; isMinimized: boolean;
@ -69,6 +70,7 @@ interface IState {
selectedSpace?: Room; selectedSpace?: Room;
} }
@replaceableComponent("structures.UserMenu")
export default class UserMenu extends React.Component<IProps, IState> { export default class UserMenu extends React.Component<IProps, IState> {
private dispatcherRef: string; private dispatcherRef: string;
private themeWatcherRef: string; private themeWatcherRef: string;

View file

@ -23,7 +23,9 @@ import * as sdk from "../../index";
import Modal from '../../Modal'; import Modal from '../../Modal';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import HomePage from "./HomePage"; import HomePage from "./HomePage";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.UserView")
export default class UserView extends React.Component { export default class UserView extends React.Component {
static get propTypes() { static get propTypes() {
return { return {

View file

@ -21,28 +21,59 @@ import PropTypes from 'prop-types';
import SyntaxHighlight from '../views/elements/SyntaxHighlight'; import SyntaxHighlight from '../views/elements/SyntaxHighlight';
import {_t} from "../../languageHandler"; import {_t} from "../../languageHandler";
import * as sdk from "../../index"; import * as sdk from "../../index";
import {replaceableComponent} from "../../utils/replaceableComponent";
@replaceableComponent("structures.ViewSource")
export default class ViewSource extends React.Component { export default class ViewSource extends React.Component {
static propTypes = { static propTypes = {
content: PropTypes.object.isRequired, content: PropTypes.object.isRequired,
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
eventId: PropTypes.string.isRequired, eventId: PropTypes.string.isRequired,
isEncrypted: PropTypes.bool.isRequired,
decryptedContent: PropTypes.object,
}; };
render() { render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
<div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
<div className="mx_ViewSource_label_right">Event ID: { this.props.eventId }</div>
<div className="mx_ViewSource_label_bottom" />
<div className="mx_Dialog_content"> let content;
if (this.props.isEncrypted) {
content = <>
<details open className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{_t("Decrypted event source")}</span>
</summary>
<SyntaxHighlight className="json">
{ JSON.stringify(this.props.decryptedContent, null, 2) }
</SyntaxHighlight>
</details>
<details className="mx_ViewSource_details">
<summary>
<span className="mx_ViewSource_heading">{_t("Original event source")}</span>
</summary>
<SyntaxHighlight className="json"> <SyntaxHighlight className="json">
{ JSON.stringify(this.props.content, null, 2) } { JSON.stringify(this.props.content, null, 2) }
</SyntaxHighlight> </SyntaxHighlight>
</details>
</>;
} else {
content = <>
<div className="mx_ViewSource_heading">{_t("Original event source")}</div>
<SyntaxHighlight className="json">
{ JSON.stringify(this.props.content, null, 2) }
</SyntaxHighlight>
</>;
}
return (
<BaseDialog className="mx_ViewSource" onFinished={this.props.onFinished} title={_t('View Source')}>
<div className="mx_Dialog_content">
<div className="mx_ViewSource_label_left">Room ID: { this.props.roomId }</div>
<div className="mx_ViewSource_label_left">Event ID: { this.props.eventId }</div>
<div className="mx_ViewSource_separator" />
{ content }
</div> </div>
</BaseDialog> </BaseDialog>
); );

View file

@ -20,13 +20,16 @@ import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO, PHASE_INTRO,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
PHASE_CONFIRM_SKIP, PHASE_CONFIRM_SKIP,
} from '../../../stores/SetupEncryptionStore'; } from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody"; import SetupEncryptionBody from "./SetupEncryptionBody";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.CompleteSecurity")
export default class CompleteSecurity extends React.Component { export default class CompleteSecurity extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
@ -58,7 +61,9 @@ export default class CompleteSecurity extends React.Component {
let icon; let icon;
let title; let title;
if (phase === PHASE_INTRO) { if (phase === PHASE_LOADING) {
return null;
} else if (phase === PHASE_INTRO) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />; icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Verify this login"); title = _t("Verify this login");
} else if (phase === PHASE_DONE) { } else if (phase === PHASE_DONE) {

View file

@ -19,7 +19,9 @@ import PropTypes from 'prop-types';
import AuthPage from '../../views/auth/AuthPage'; import AuthPage from '../../views/auth/AuthPage';
import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody'; import CompleteSecurityBody from '../../views/auth/CompleteSecurityBody';
import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog'; import CreateCrossSigningDialog from '../../views/dialogs/security/CreateCrossSigningDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("structures.auth.E2eSetup")
export default class E2eSetup extends React.Component { export default class E2eSetup extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,

View file

@ -27,6 +27,7 @@ import classNames from 'classnames';
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import ServerPicker from "../../views/elements/ServerPicker"; import ServerPicker from "../../views/elements/ServerPicker";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// Phases // Phases
// Show the forgot password inputs // Show the forgot password inputs
@ -38,6 +39,7 @@ const PHASE_EMAIL_SENT = 3;
// User has clicked the link in email and completed reset // User has clicked the link in email and completed reset
const PHASE_DONE = 4; const PHASE_DONE = 4;
@replaceableComponent("structures.auth.ForgotPassword")
export default class ForgotPassword extends React.Component { export default class ForgotPassword extends React.Component {
static propTypes = { static propTypes = {
serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired, serverConfig: PropTypes.instanceOf(ValidatedServerConfig).isRequired,

View file

@ -35,6 +35,7 @@ import InlineSpinner from "../../views/elements/InlineSpinner";
import Spinner from "../../views/elements/Spinner"; import Spinner from "../../views/elements/Spinner";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from "../../views/elements/ServerPicker"; import ServerPicker from "../../views/elements/ServerPicker";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// These are used in several places, and come from the js-sdk's autodiscovery // These are used in several places, and come from the js-sdk's autodiscovery
// stuff. We define them here so that they'll be picked up by i18n. // stuff. We define them here so that they'll be picked up by i18n.
@ -99,6 +100,7 @@ interface IState {
/* /*
* A wire component which glues together login UI components and Login logic * A wire component which glues together login UI components and Login logic
*/ */
@replaceableComponent("structures.auth.LoginComponent")
export default class LoginComponent extends React.PureComponent<IProps, IState> { export default class LoginComponent extends React.PureComponent<IProps, IState> {
private unmounted = false; private unmounted = false;
private loginLogic: Login; private loginLogic: Login;
@ -218,6 +220,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
'monthly_active_user': _td( 'monthly_active_user': _td(
"This homeserver has hit its Monthly Active User limit.", "This homeserver has hit its Monthly Active User limit.",
), ),
'hs_blocked': _td(
"This homeserver has been blocked by it's administrator.",
),
'': _td( '': _td(
"This homeserver has exceeded one of its resource limits.", "This homeserver has exceeded one of its resource limits.",
), ),

View file

@ -30,6 +30,7 @@ import Login, {ISSOFlow} from "../../../Login";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import ServerPicker from '../../views/elements/ServerPicker'; import ServerPicker from '../../views/elements/ServerPicker';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
serverConfig: ValidatedServerConfig; serverConfig: ValidatedServerConfig;
@ -109,6 +110,7 @@ interface IState {
ssoFlow?: ISSOFlow; ssoFlow?: ISSOFlow;
} }
@replaceableComponent("structures.auth.Registration")
export default class Registration extends React.Component<IProps, IState> { export default class Registration extends React.Component<IProps, IState> {
loginLogic: Login; loginLogic: Login;
@ -276,6 +278,7 @@ export default class Registration extends React.Component<IProps, IState> {
response.data.admin_contact, response.data.admin_contact,
{ {
'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."), 'monthly_active_user': _td("This homeserver has hit its Monthly Active User limit."),
'hs_blocked': _td("This homeserver has been blocked by it's administrator."),
'': _td("This homeserver has exceeded one of its resource limits."), '': _td("This homeserver has exceeded one of its resource limits."),
}, },
); );

View file

@ -17,17 +17,20 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import {
SetupEncryptionStore, SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO, PHASE_INTRO,
PHASE_BUSY, PHASE_BUSY,
PHASE_DONE, PHASE_DONE,
PHASE_CONFIRM_SKIP, PHASE_CONFIRM_SKIP,
PHASE_FINISHED, PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore'; } from '../../../stores/SetupEncryptionStore';
import {replaceableComponent} from "../../../utils/replaceableComponent";
function keyHasPassphrase(keyInfo) { function keyHasPassphrase(keyInfo) {
return ( return (
@ -37,6 +40,7 @@ function keyHasPassphrase(keyInfo) {
); );
} }
@replaceableComponent("structures.auth.SetupEncryptionBody")
export default class SetupEncryptionBody extends React.Component { export default class SetupEncryptionBody extends React.Component {
static propTypes = { static propTypes = {
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
@ -81,6 +85,22 @@ export default class SetupEncryptionBody extends React.Component {
store.usePassPhrase(); store.usePassPhrase();
} }
_onVerifyClick = () => {
const cli = MatrixClientPeg.get();
const userId = cli.getUserId();
const requestPromise = cli.requestVerification(userId);
this.props.onFinished(true);
Modal.createTrackedDialog('New Session Verification', 'Starting dialog', VerificationRequestDialog, {
verificationRequestPromise: requestPromise,
member: cli.getUser(userId),
onFinished: async () => {
const request = await requestPromise;
request.cancel();
},
});
}
onSkipClick = () => { onSkipClick = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
store.skip(); store.skip();
@ -132,32 +152,22 @@ export default class SetupEncryptionBody extends React.Component {
</AccessibleButton>; </AccessibleButton>;
} }
const brand = SdkConfig.get().brand; let verifyButton;
if (store.hasDevicesToVerifyAgainst) {
verifyButton = <AccessibleButton kind="primary" onClick={this._onVerifyClick}>
{ _t("Verify with another session") }
</AccessibleButton>;
}
return ( return (
<div> <div>
<p>{_t( <p>{_t(
"Confirm your identity by verifying this login from one of your other sessions, " + "Verify this login to access your encrypted messages and " +
"granting it access to encrypted messages.", "prove to others that this login is really you.",
)}</p> )}</p>
<p>{_t(
"This requires the latest %(brand)s on your other devices:",
{ brand },
)}</p>
<div className="mx_CompleteSecurity_clients">
<div className="mx_CompleteSecurity_clients_desktop">
<div>{_t("%(brand)s Web", { brand })}</div>
<div>{_t("%(brand)s Desktop", { brand })}</div>
</div>
<div className="mx_CompleteSecurity_clients_mobile">
<div>{_t("%(brand)s iOS", { brand })}</div>
<div>{_t("%(brand)s Android", { brand })}</div>
</div>
<p>{_t("or another cross-signing capable Matrix client")}</p>
</div>
<div className="mx_CompleteSecurity_actionRow"> <div className="mx_CompleteSecurity_actionRow">
{verifyButton}
{useRecoveryKeyButton} {useRecoveryKeyButton}
<AccessibleButton kind="danger" onClick={this.onSkipClick}> <AccessibleButton kind="danger" onClick={this.onSkipClick}>
{_t("Skip")} {_t("Skip")}
@ -215,7 +225,7 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
</div> </div>
); );
} else if (phase === PHASE_BUSY) { } else if (phase === PHASE_BUSY || phase === PHASE_LOADING) {
const Spinner = sdk.getComponent('views.elements.Spinner'); const Spinner = sdk.getComponent('views.elements.Spinner');
return <Spinner />; return <Spinner />;
} else { } else {

View file

@ -26,6 +26,7 @@ import {sendLoginRequest} from "../../../Login";
import AuthPage from "../../views/auth/AuthPage"; import AuthPage from "../../views/auth/AuthPage";
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 SSOButtons from "../../views/elements/SSOButtons"; import SSOButtons from "../../views/elements/SSOButtons";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const LOGIN_VIEW = { const LOGIN_VIEW = {
LOADING: 1, LOADING: 1,
@ -41,6 +42,7 @@ const FLOWS_TO_VIEWS = {
"m.login.sso": LOGIN_VIEW.SSO, "m.login.sso": LOGIN_VIEW.SSO,
}; };
@replaceableComponent("structures.auth.SoftLogout")
export default class SoftLogout extends React.Component { export default class SoftLogout extends React.Component {
static propTypes = { static propTypes = {
// Query parameters from MatrixChat // Query parameters from MatrixChat

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthBody")
export default class AuthBody extends React.PureComponent { export default class AuthBody extends React.PureComponent {
render() { render() {
return <div className="mx_AuthBody"> return <div className="mx_AuthBody">

View file

@ -18,7 +18,9 @@ limitations under the License.
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import React from 'react'; import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthFooter")
export default class AuthFooter extends React.Component { export default class AuthFooter extends React.Component {
render() { render() {
return ( return (

View file

@ -18,7 +18,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeader")
export default class AuthHeader extends React.Component { export default class AuthHeader extends React.Component {
static propTypes = { static propTypes = {
disableLanguageSelector: PropTypes.bool, disableLanguageSelector: PropTypes.bool,

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.AuthHeaderLogo")
export default class AuthHeaderLogo extends React.PureComponent { export default class AuthHeaderLogo extends React.PureComponent {
render() { render() {
return <div className="mx_AuthHeaderLogo"> return <div className="mx_AuthHeaderLogo">

View file

@ -18,12 +18,14 @@ import React, {createRef} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const DIV_ID = 'mx_recaptcha'; const DIV_ID = 'mx_recaptcha';
/** /**
* A pure UI component which displays a captcha form. * A pure UI component which displays a captcha form.
*/ */
@replaceableComponent("views.auth.CaptchaForm")
export default class CaptchaForm extends React.Component { export default class CaptchaForm extends React.Component {
static propTypes = { static propTypes = {
sitePublicKey: PropTypes.string, sitePublicKey: PropTypes.string,

View file

@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
'use strict';
import React from 'react'; import React from 'react';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.auth.CompleteSecurityBody")
export default class CompleteSecurityBody extends React.PureComponent { export default class CompleteSecurityBody extends React.PureComponent {
render() { render() {
return <div className="mx_CompleteSecurityBody"> return <div className="mx_CompleteSecurityBody">

View file

@ -22,6 +22,7 @@ import * as sdk from '../../../index';
import {COUNTRIES, getEmojiFlag} from '../../../phonenumber'; import {COUNTRIES, getEmojiFlag} from '../../../phonenumber';
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import { _t } from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import {replaceableComponent} from "../../../utils/replaceableComponent";
const COUNTRIES_BY_ISO2 = {}; const COUNTRIES_BY_ISO2 = {};
for (const c of COUNTRIES) { for (const c of COUNTRIES) {
@ -40,6 +41,7 @@ function countryMatchesSearchQuery(query, country) {
return false; return false;
} }
@replaceableComponent("views.auth.CountryDropdown")
export default class CountryDropdown extends React.Component { export default class CountryDropdown extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -26,6 +26,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import Spinner from "../elements/Spinner"; import Spinner from "../elements/Spinner";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
/* This file contains a collection of components which are used by the /* This file contains a collection of components which are used by the
* InteractiveAuth to prompt the user to enter the information needed * InteractiveAuth to prompt the user to enter the information needed
@ -75,6 +76,7 @@ import CountlyAnalytics from "../../../CountlyAnalytics";
export const DEFAULT_PHASE = 0; export const DEFAULT_PHASE = 0;
@replaceableComponent("views.auth.PasswordAuthEntry")
export class PasswordAuthEntry extends React.Component { export class PasswordAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.password"; static LOGIN_TYPE = "m.login.password";
@ -173,6 +175,7 @@ export class PasswordAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.RecaptchaAuthEntry")
export class RecaptchaAuthEntry extends React.Component { export class RecaptchaAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.recaptcha"; static LOGIN_TYPE = "m.login.recaptcha";
@ -235,6 +238,7 @@ export class RecaptchaAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.TermsAuthEntry")
export class TermsAuthEntry extends React.Component { export class TermsAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.terms"; static LOGIN_TYPE = "m.login.terms";
@ -385,6 +389,7 @@ export class TermsAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.EmailIdentityAuthEntry")
export class EmailIdentityAuthEntry extends React.Component { export class EmailIdentityAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.email.identity"; static LOGIN_TYPE = "m.login.email.identity";
@ -432,6 +437,7 @@ export class EmailIdentityAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.MsisdnAuthEntry")
export class MsisdnAuthEntry extends React.Component { export class MsisdnAuthEntry extends React.Component {
static LOGIN_TYPE = "m.login.msisdn"; static LOGIN_TYPE = "m.login.msisdn";
@ -578,6 +584,7 @@ export class MsisdnAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.SSOAuthEntry")
export class SSOAuthEntry extends React.Component { export class SSOAuthEntry extends React.Component {
static propTypes = { static propTypes = {
matrixClient: PropTypes.object.isRequired, matrixClient: PropTypes.object.isRequired,
@ -708,6 +715,7 @@ export class SSOAuthEntry extends React.Component {
} }
} }
@replaceableComponent("views.auth.FallbackAuthEntry")
export class FallbackAuthEntry extends React.Component { export class FallbackAuthEntry extends React.Component {
static propTypes = { static propTypes = {
matrixClient: PropTypes.object.isRequired, matrixClient: PropTypes.object.isRequired,

View file

@ -22,6 +22,7 @@ import SdkConfig from "../../../SdkConfig";
import withValidation, {IFieldState, IValidationResult} from "../elements/Validation"; import withValidation, {IFieldState, IValidationResult} from "../elements/Validation";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import Field, {IInputProps} from "../elements/Field"; import Field, {IInputProps} from "../elements/Field";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends Omit<IInputProps, "onValidate"> { interface IProps extends Omit<IInputProps, "onValidate"> {
autoFocus?: boolean; autoFocus?: boolean;
@ -40,6 +41,7 @@ interface IProps extends Omit<IInputProps, "onValidate"> {
onValidate(result: IValidationResult); onValidate(result: IValidationResult);
} }
@replaceableComponent("views.auth.PassphraseField")
class PassphraseField extends PureComponent<IProps> { class PassphraseField extends PureComponent<IProps> {
static defaultProps = { static defaultProps = {
label: _td("Password"), label: _td("Password"),

View file

@ -26,6 +26,7 @@ import withValidation from "../elements/Validation";
import * as Email from "../../../email"; import * as Email from "../../../email";
import Field from "../elements/Field"; import Field from "../elements/Field";
import CountryDropdown from "./CountryDropdown"; import CountryDropdown from "./CountryDropdown";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// For validating phone numbers without country codes // For validating phone numbers without country codes
const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/; const PHONE_NUMBER_REGEX = /^[0-9()\-\s]*$/;
@ -66,6 +67,7 @@ enum LoginField {
* A pure UI component which displays a username/password form. * A pure UI component which displays a username/password form.
* The email/username/phone fields are fully-controlled, the password field is not. * The email/username/phone fields are fully-controlled, the password field is not.
*/ */
@replaceableComponent("views.auth.PasswordLogin")
export default class PasswordLogin extends React.PureComponent<IProps, IState> { export default class PasswordLogin extends React.PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
onUsernameChanged: function() {}, onUsernameChanged: function() {},

View file

@ -30,6 +30,7 @@ import PassphraseField from "./PassphraseField";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import Field from '../elements/Field'; import Field from '../elements/Field';
import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog'; import RegistrationEmailPromptDialog from '../dialogs/RegistrationEmailPromptDialog';
import {replaceableComponent} from "../../../utils/replaceableComponent";
enum RegistrationField { enum RegistrationField {
Email = "field_email", Email = "field_email",
@ -80,6 +81,7 @@ interface IState {
/* /*
* A pure UI component which displays a registration form. * A pure UI component which displays a registration form.
*/ */
@replaceableComponent("views.auth.RegistrationForm")
export default class RegistrationForm extends React.PureComponent<IProps, IState> { export default class RegistrationForm extends React.PureComponent<IProps, IState> {
static defaultProps = { static defaultProps = {
onValidationChange: console.error, onValidationChange: console.error,

View file

@ -24,10 +24,12 @@ import {_td} from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {UIFeature} from "../../../settings/UIFeature"; import {UIFeature} from "../../../settings/UIFeature";
import CountlyAnalytics from "../../../CountlyAnalytics"; import CountlyAnalytics from "../../../CountlyAnalytics";
import {replaceableComponent} from "../../../utils/replaceableComponent";
// translatable strings for Welcome pages // translatable strings for Welcome pages
_td("Sign in with SSO"); _td("Sign in with SSO");
@replaceableComponent("views.auth.Welcome")
export default class Welcome extends React.PureComponent { export default class Welcome extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -30,6 +30,7 @@ import {MatrixClientPeg} from "../../../MatrixClientPeg";
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import TextWithTooltip from "../elements/TextWithTooltip"; import TextWithTooltip from "../elements/TextWithTooltip";
import DMRoomMap from "../../../utils/DMRoomMap"; import DMRoomMap from "../../../utils/DMRoomMap";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps { interface IProps {
room: Room; room: Room;
@ -68,6 +69,7 @@ function tooltipText(variant: Icon) {
} }
} }
@replaceableComponent("views.avatars.DecoratedRoomAvatar")
export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> { export default class DecoratedRoomAvatar extends React.PureComponent<IProps, IState> {
private _dmUser: User; private _dmUser: User;
private isUnmounted = false; private isUnmounted = false;

View file

@ -17,6 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import BaseAvatar from './BaseAvatar'; import BaseAvatar from './BaseAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent";
export interface IProps { export interface IProps {
groupId?: string; groupId?: string;
@ -28,6 +29,7 @@ export interface IProps {
onClick?: React.MouseEventHandler; onClick?: React.MouseEventHandler;
} }
@replaceableComponent("views.avatars.GroupAvatar")
export default class GroupAvatar extends React.Component<IProps> { export default class GroupAvatar extends React.Component<IProps> {
public static defaultProps = { public static defaultProps = {
width: 36, width: 36,

View file

@ -22,6 +22,7 @@ import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {MatrixClientPeg} from "../../../MatrixClientPeg"; import {MatrixClientPeg} from "../../../MatrixClientPeg";
import BaseAvatar from "./BaseAvatar"; import BaseAvatar from "./BaseAvatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> { interface IProps extends Omit<React.ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url"> {
member: RoomMember; member: RoomMember;
@ -42,6 +43,7 @@ interface IState {
imageUrl?: string; imageUrl?: string;
} }
@replaceableComponent("views.avatars.MemberAvatar")
export default class MemberAvatar extends React.Component<IProps, IState> { export default class MemberAvatar extends React.Component<IProps, IState> {
public static defaultProps = { public static defaultProps = {
width: 40, width: 40,

View file

@ -23,7 +23,9 @@ import classNames from 'classnames';
import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu"; import StatusMessageContextMenu from "../context_menus/StatusMessageContextMenu";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu"; import {ContextMenu, ContextMenuButton} from "../../structures/ContextMenu";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.avatars.MemberStatusMessageAvatar")
export default class MemberStatusMessageAvatar extends React.Component { export default class MemberStatusMessageAvatar extends React.Component {
static propTypes = { static propTypes = {
member: PropTypes.object.isRequired, member: PropTypes.object.isRequired,

View file

@ -23,6 +23,7 @@ import {MatrixClientPeg} from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import * as Avatar from '../../../Avatar'; import * as Avatar from '../../../Avatar';
import {ResizeMethod} from "../../../Avatar"; import {ResizeMethod} from "../../../Avatar";
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> { interface IProps extends Omit<ComponentProps<typeof BaseAvatar>, "name" | "idName" | "url" | "onClick"> {
// Room may be left unset here, but if it is, // Room may be left unset here, but if it is,
@ -42,6 +43,7 @@ interface IState {
urls: string[]; urls: string[];
} }
@replaceableComponent("views.avatars.RoomAvatar")
export default class RoomAvatar extends React.Component<IProps, IState> { export default class RoomAvatar extends React.Component<IProps, IState> {
public static defaultProps = { public static defaultProps = {
width: 36, width: 36,

View file

@ -22,11 +22,13 @@ import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import CallHandler from '../../../CallHandler'; import CallHandler from '../../../CallHandler';
import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog'; import InviteDialog, { KIND_CALL_TRANSFER } from '../dialogs/InviteDialog';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
call: MatrixCall; call: MatrixCall;
} }
@replaceableComponent("views.context_menus.CallContextMenu")
export default class CallContextMenu extends React.Component<IProps> { export default class CallContextMenu extends React.Component<IProps> {
static propTypes = { static propTypes = {
// js-sdk User object. Not required because it might not exist. // js-sdk User object. Not required because it might not exist.

View file

@ -19,6 +19,7 @@ import { _t } from '../../../languageHandler';
import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu';
import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call';
import Dialpad from '../voip/DialPad'; import Dialpad from '../voip/DialPad';
import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps extends IContextMenuProps { interface IProps extends IContextMenuProps {
call: MatrixCall; call: MatrixCall;
@ -28,6 +29,7 @@ interface IState {
value: string; value: string;
} }
@replaceableComponent("views.context_menus.DialpadContextMenu")
export default class DialpadContextMenu extends React.Component<IProps, IState> { export default class DialpadContextMenu extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/* /*
* This component can be used to display generic HTML content in a contextual * This component can be used to display generic HTML content in a contextual
@ -23,6 +24,7 @@ import PropTypes from 'prop-types';
*/ */
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component { export default class GenericElementContextMenu extends React.Component {
static propTypes = { static propTypes = {
element: PropTypes.element.isRequired, element: PropTypes.element.isRequired,

View file

@ -16,7 +16,9 @@ limitations under the License.
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.context_menus.GenericTextContextMenu")
export default class GenericTextContextMenu extends React.Component { export default class GenericTextContextMenu extends React.Component {
static propTypes = { static propTypes = {
message: PropTypes.string.isRequired, message: PropTypes.string.isRequired,

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