diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec73756ff9..d459b4e94a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,121 @@
+Changes in [3.19.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0) (2021-04-26)
+=====================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.19.0-rc.1...v3.19.0)
+
+ * Upgrade to JS SDK 10.0.0
+ * [Release] Dynamic max and min zoom in the new ImageView
+ [\#5927](https://github.com/matrix-org/matrix-react-sdk/pull/5927)
+ * [Release] Add a WheelEvent normalization function
+ [\#5911](https://github.com/matrix-org/matrix-react-sdk/pull/5911)
+ * Add a WheelEvent normalization function
+ [\#5904](https://github.com/matrix-org/matrix-react-sdk/pull/5904)
+ * [Release] Use floats for image background opacity
+ [\#5907](https://github.com/matrix-org/matrix-react-sdk/pull/5907)
+
+Changes in [3.19.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.19.0-rc.1) (2021-04-21)
+===============================================================================================================
+[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0...v3.19.0-rc.1)
+
+ * Upgrade to JS SDK 10.0.0-rc.1
+ * Translations update from Weblate
+ [\#5896](https://github.com/matrix-org/matrix-react-sdk/pull/5896)
+ * Fix sticky tags header in room list
+ [\#5895](https://github.com/matrix-org/matrix-react-sdk/pull/5895)
+ * Fix spaces filtering sometimes lagging behind or behaving oddly
+ [\#5893](https://github.com/matrix-org/matrix-react-sdk/pull/5893)
+ * Fix issue with spaces context switching looping and breaking
+ [\#5894](https://github.com/matrix-org/matrix-react-sdk/pull/5894)
+ * Improve RoomList render time when filtering
+ [\#5874](https://github.com/matrix-org/matrix-react-sdk/pull/5874)
+ * Avoid being stuck in a space
+ [\#5891](https://github.com/matrix-org/matrix-react-sdk/pull/5891)
+ * [Spaces] Context switching
+ [\#5795](https://github.com/matrix-org/matrix-react-sdk/pull/5795)
+ * Warn when you attempt to leave room that you are the only member of
+ [\#5415](https://github.com/matrix-org/matrix-react-sdk/pull/5415)
+ * Ensure PersistedElement are unmounted on application logout
+ [\#5884](https://github.com/matrix-org/matrix-react-sdk/pull/5884)
+ * Add missing space in seshat dialog and the corresponding string
+ [\#5866](https://github.com/matrix-org/matrix-react-sdk/pull/5866)
+ * A tiny change to make the Add existing rooms dialog a little nicer
+ [\#5885](https://github.com/matrix-org/matrix-react-sdk/pull/5885)
+ * Remove weird margin from the file panel
+ [\#5889](https://github.com/matrix-org/matrix-react-sdk/pull/5889)
+ * Trigger lazy loading when filtering using spaces
+ [\#5882](https://github.com/matrix-org/matrix-react-sdk/pull/5882)
+ * Fix typo in method call in add existing to space dialog
+ [\#5883](https://github.com/matrix-org/matrix-react-sdk/pull/5883)
+ * New Image View fixes/improvements
+ [\#5872](https://github.com/matrix-org/matrix-react-sdk/pull/5872)
+ * Limit voice recording length
+ [\#5871](https://github.com/matrix-org/matrix-react-sdk/pull/5871)
+ * Clean up add existing to space dialog and include DMs in it too
+ [\#5881](https://github.com/matrix-org/matrix-react-sdk/pull/5881)
+ * Fix unknown slash command error exploding
+ [\#5853](https://github.com/matrix-org/matrix-react-sdk/pull/5853)
+ * Switch to a spec conforming email validation Regexp
+ [\#5852](https://github.com/matrix-org/matrix-react-sdk/pull/5852)
+ * Cleanup unused state in MessageComposer
+ [\#5877](https://github.com/matrix-org/matrix-react-sdk/pull/5877)
+ * Pulse animation for voice messages recording state
+ [\#5869](https://github.com/matrix-org/matrix-react-sdk/pull/5869)
+ * Don't include invisible rooms in notify summary
+ [\#5875](https://github.com/matrix-org/matrix-react-sdk/pull/5875)
+ * Properly disable composer access when recording a voice message
+ [\#5870](https://github.com/matrix-org/matrix-react-sdk/pull/5870)
+ * Stabilise starting a DM with multiple people flow
+ [\#5862](https://github.com/matrix-org/matrix-react-sdk/pull/5862)
+ * Render msgOption only if showReadReceipts is enabled
+ [\#5864](https://github.com/matrix-org/matrix-react-sdk/pull/5864)
+ * Labs: Add quick/cheap "do not disturb" flag
+ [\#5873](https://github.com/matrix-org/matrix-react-sdk/pull/5873)
+ * Fix ReadReceipts animations
+ [\#5836](https://github.com/matrix-org/matrix-react-sdk/pull/5836)
+ * Add tooltips to message previews
+ [\#5859](https://github.com/matrix-org/matrix-react-sdk/pull/5859)
+ * IRC Layout fix layout spacing in replies
+ [\#5855](https://github.com/matrix-org/matrix-react-sdk/pull/5855)
+ * Move user to welcome_page if continuing with previous session
+ [\#5849](https://github.com/matrix-org/matrix-react-sdk/pull/5849)
+ * Improve image view
+ [\#5521](https://github.com/matrix-org/matrix-react-sdk/pull/5521)
+ * Add a button to reset personal encryption state during login
+ [\#5819](https://github.com/matrix-org/matrix-react-sdk/pull/5819)
+ * Fix js-sdk import in SlashCommands
+ [\#5850](https://github.com/matrix-org/matrix-react-sdk/pull/5850)
+ * Fix useRoomPowerLevels hook
+ [\#5854](https://github.com/matrix-org/matrix-react-sdk/pull/5854)
+ * Prevent state events being rendered with invalid state keys
+ [\#5851](https://github.com/matrix-org/matrix-react-sdk/pull/5851)
+ * Give server ACLs a name in 'roles & permissions' tab
+ [\#5838](https://github.com/matrix-org/matrix-react-sdk/pull/5838)
+ * Don't hide notification badge on the home space button as it has no menu
+ [\#5845](https://github.com/matrix-org/matrix-react-sdk/pull/5845)
+ * User Info hide disambiguation as we always show MXID anyway
+ [\#5843](https://github.com/matrix-org/matrix-react-sdk/pull/5843)
+ * Improve kick state to not show if the target was not joined to begin with
+ [\#5846](https://github.com/matrix-org/matrix-react-sdk/pull/5846)
+ * Fix space store wrongly switching to a non-space filter
+ [\#5844](https://github.com/matrix-org/matrix-react-sdk/pull/5844)
+ * Tweak appearance of invite reason
+ [\#5847](https://github.com/matrix-org/matrix-react-sdk/pull/5847)
+ * Update Inter font to v3.18
+ [\#5840](https://github.com/matrix-org/matrix-react-sdk/pull/5840)
+ * Enable sharing historical keys on invite
+ [\#5839](https://github.com/matrix-org/matrix-react-sdk/pull/5839)
+ * Add ability to hide post-login encryption setup with customisation point
+ [\#5834](https://github.com/matrix-org/matrix-react-sdk/pull/5834)
+ * Use LaTeX and TeX delimiters by default
+ [\#5515](https://github.com/matrix-org/matrix-react-sdk/pull/5515)
+ * Clone author's deps fork for Netlify previews
+ [\#5837](https://github.com/matrix-org/matrix-react-sdk/pull/5837)
+ * Show drop file UI only if dragging a file
+ [\#5827](https://github.com/matrix-org/matrix-react-sdk/pull/5827)
+ * Ignore punctuation when filtering rooms
+ [\#5824](https://github.com/matrix-org/matrix-react-sdk/pull/5824)
+ * Resizable CallView
+ [\#5710](https://github.com/matrix-org/matrix-react-sdk/pull/5710)
+
Changes in [3.18.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.18.0) (2021-04-12)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.18.0-rc.1...v3.18.0)
diff --git a/package.json b/package.json
index 7c190c68bf..39c3b68103 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "matrix-react-sdk",
- "version": "3.18.0",
+ "version": "3.19.0",
"description": "SDK for matrix.org using React",
"author": "matrix.org",
"repository": {
@@ -23,9 +23,7 @@
"package.json"
],
"bin": {
- "reskindex": "scripts/reskindex.js",
- "matrix-gen-i18n": "scripts/gen-i18n.js",
- "matrix-prune-i18n": "scripts/prune-i18n.js"
+ "reskindex": "scripts/reskindex.js"
},
"main": "./src/index.js",
"matrix_src_main": "./src/index.js",
@@ -35,7 +33,7 @@
"prepublishOnly": "yarn build",
"i18n": "matrix-gen-i18n",
"prunei18n": "matrix-prune-i18n",
- "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && ./scripts/gen-i18n.js && node scripts/compare-file.js src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
+ "diff-i18n": "cp src/i18n/strings/en_EN.json src/i18n/strings/en_EN_orig.json && matrix-gen-i18n && matrix-compare-i18n-files src/i18n/strings/en_EN_orig.json src/i18n/strings/en_EN.json",
"reskindex": "node scripts/reskindex.js -h header",
"reskindex:watch": "node scripts/reskindex.js -h header -w",
"rethemendex": "res/css/rethemendex.sh",
@@ -160,6 +158,7 @@
"jest-fetch-mock": "^3.0.3",
"matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2",
+ "matrix-web-i18n": "github:matrix-org/matrix-web-i18n",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"react-test-renderer": "^16.14.0",
"rimraf": "^3.0.2",
diff --git a/res/css/structures/_RoomStatusBar.scss b/res/css/structures/_RoomStatusBar.scss
index 5bf2aee3ae..8cc00aba0f 100644
--- a/res/css/structures/_RoomStatusBar.scss
+++ b/res/css/structures/_RoomStatusBar.scss
@@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-.mx_RoomStatusBar {
+.mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
margin-left: 65px;
min-height: 50px;
}
@@ -68,6 +68,99 @@ limitations under the License.
min-height: 58px;
}
+.mx_RoomStatusBar_unsentMessages {
+ > div[role="alert"] {
+ // cheat some basic alignment
+ display: flex;
+ align-items: center;
+ min-height: 70px;
+ margin: 12px;
+ padding-left: 16px;
+ background-color: $header-panel-bg-color;
+ border-radius: 4px;
+ }
+
+ .mx_RoomStatusBar_unsentBadge {
+ margin-right: 12px;
+
+ .mx_NotificationBadge {
+ // Override sizing from the default badge
+ width: 24px !important;
+ height: 24px !important;
+ border-radius: 24px !important;
+
+ .mx_NotificationBadge_count {
+ font-size: $font-16px !important; // override default
+ }
+ }
+ }
+
+ .mx_RoomStatusBar_unsentTitle {
+ color: $warning-color;
+ font-size: $font-15px;
+ }
+
+ .mx_RoomStatusBar_unsentDescription {
+ font-size: $font-12px;
+ }
+
+ .mx_RoomStatusBar_unsentButtonBar {
+ flex-grow: 1;
+ text-align: right;
+ margin-right: 22px;
+ color: $muted-fg-color;
+
+ .mx_AccessibleButton {
+ padding: 5px 10px;
+ padding-left: 28px; // 16px for the icon, 2px margin to text, 10px regular padding
+ display: inline-block;
+ position: relative;
+
+ &:nth-child(2) {
+ border-left: 1px solid $resend-button-divider-color;
+ }
+
+ &::before {
+ content: '';
+ position: absolute;
+ left: 10px; // inset for regular button padding
+ background-color: $muted-fg-color;
+ mask-repeat: no-repeat;
+ mask-position: center;
+ mask-size: contain;
+ }
+
+ &.mx_RoomStatusBar_unsentCancelAllBtn::before {
+ mask-image: url('$(res)/img/element-icons/trashcan.svg');
+ width: 12px;
+ height: 16px;
+ top: calc(50% - 8px); // text sizes are dynamic
+ }
+
+ &.mx_RoomStatusBar_unsentResendAllBtn {
+ padding-left: 34px; // 28px from above, but +6px to account for the wider icon
+
+ &::before {
+ mask-image: url('$(res)/img/element-icons/retry.svg');
+ width: 18px;
+ height: 18px;
+ top: calc(50% - 9px); // text sizes are dynamic
+ }
+ }
+ }
+
+ .mx_InlineSpinner {
+ vertical-align: middle;
+ margin-right: 5px;
+ top: 1px; // just to help the vertical alignment be slightly better
+
+ & + span {
+ margin-right: 10px; // same margin/padding as the rightmost button
+ }
+ }
+ }
+}
+
.mx_RoomStatusBar_connectionLostBar img {
padding-left: 10px;
padding-right: 10px;
@@ -103,7 +196,7 @@ limitations under the License.
}
.mx_MatrixChat_useCompactLayout {
- .mx_RoomStatusBar {
+ .mx_RoomStatusBar:not(.mx_RoomStatusBar_unsentMessages) {
min-height: 40px;
}
diff --git a/res/css/structures/_SpaceRoomDirectory.scss b/res/css/structures/_SpaceRoomDirectory.scss
index dcceee6371..c7d087d8e0 100644
--- a/res/css/structures/_SpaceRoomDirectory.scss
+++ b/res/css/structures/_SpaceRoomDirectory.scss
@@ -26,7 +26,10 @@ limitations under the License.
word-break: break-word;
display: flex;
flex-direction: column;
+}
+.mx_SpaceRoomDirectory,
+.mx_SpaceRoomView_landing {
.mx_Dialog_title {
display: flex;
@@ -56,65 +59,63 @@ limitations under the License.
}
}
- .mx_Dialog_content {
- .mx_AccessibleButton_kind_link {
- padding: 0;
- }
+ .mx_AccessibleButton_kind_link {
+ padding: 0;
+ }
- .mx_SearchBox {
- margin: 24px 0 16px;
- }
+ .mx_SearchBox {
+ margin: 24px 0 16px;
+ }
- .mx_SpaceRoomDirectory_noResults {
- text-align: center;
+ .mx_SpaceRoomDirectory_noResults {
+ text-align: center;
- > div {
- font-size: $font-15px;
- line-height: $font-24px;
- color: $secondary-fg-color;
- }
- }
-
- .mx_SpaceRoomDirectory_listHeader {
- display: flex;
- min-height: 32px;
- align-items: center;
+ > div {
font-size: $font-15px;
line-height: $font-24px;
- color: $primary-fg-color;
+ color: $secondary-fg-color;
+ }
+ }
- .mx_AccessibleButton {
- padding: 2px 8px;
- font-weight: normal;
+ .mx_SpaceRoomDirectory_listHeader {
+ display: flex;
+ min-height: 32px;
+ align-items: center;
+ font-size: $font-15px;
+ line-height: $font-24px;
+ color: $primary-fg-color;
- & + .mx_AccessibleButton {
- margin-left: 16px;
- }
- }
+ .mx_AccessibleButton {
+ padding: 2px 8px;
+ font-weight: normal;
- > span {
- margin-left: auto;
+ & + .mx_AccessibleButton {
+ margin-left: 16px;
}
}
- .mx_SpaceRoomDirectory_error {
- position: relative;
- font-weight: $font-semi-bold;
- color: $notice-primary-color;
- font-size: $font-15px;
- line-height: $font-18px;
- margin: 20px auto 12px;
- padding-left: 24px;
- width: max-content;
+ > span {
+ margin-left: auto;
+ }
+ }
- &::before {
- content: "";
- position: absolute;
- height: 16px;
- width: 16px;
- left: 0;
- background-image: url("$(res)/img/element-icons/warning-badge.svg");
- }
+ .mx_SpaceRoomDirectory_error {
+ position: relative;
+ font-weight: $font-semi-bold;
+ color: $notice-primary-color;
+ font-size: $font-15px;
+ line-height: $font-18px;
+ margin: 20px auto 12px;
+ padding-left: 24px;
+ width: max-content;
+
+ &::before {
+ content: "";
+ position: absolute;
+ height: 16px;
+ width: 16px;
+ left: 0;
+ background-image: url("$(res)/img/element-icons/warning-badge.svg");
}
}
}
diff --git a/res/css/structures/_SpaceRoomView.scss b/res/css/structures/_SpaceRoomView.scss
index 2dbf0fe0fe..269f16beb7 100644
--- a/res/css/structures/_SpaceRoomView.scss
+++ b/res/css/structures/_SpaceRoomView.scss
@@ -214,12 +214,11 @@ $SpaceRoomViewInnerWidth: 428px;
.mx_SpaceRoomView_info {
display: inline-block;
- margin: 0;
+ margin: 0 auto 0 0;
}
.mx_FacePile {
display: inline-block;
- margin-left: auto;
margin-right: 12px;
.mx_FacePile_faces {
diff --git a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
index 80ad4d6c0e..247df52b4a 100644
--- a/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
+++ b/res/css/views/dialogs/_AddExistingToSpaceDialog.scss
@@ -148,12 +148,14 @@ limitations under the License.
font-size: $font-15px;
line-height: 30px;
flex-grow: 1;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ margin-right: 12px;
}
- .mx_FormButton {
- min-width: 92px;
- font-weight: normal;
- box-sizing: border-box;
+ .mx_Checkbox {
+ align-items: center;
}
}
}
@@ -192,8 +194,4 @@ limitations under the License.
padding: 0;
}
}
-
- .mx_FormButton {
- padding: 8px 22px;
- }
}
diff --git a/res/css/views/elements/_ImageView.scss b/res/css/views/elements/_ImageView.scss
index 93ebcc2d56..71035dadc3 100644
--- a/res/css/views/elements/_ImageView.scss
+++ b/res/css/views/elements/_ImageView.scss
@@ -31,8 +31,7 @@ limitations under the License.
.mx_ImageView_image {
pointer-events: all;
- max-width: 95%;
- max-height: 95%;
+ flex-shrink: 0;
}
.mx_ImageView_panel {
diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss
index 1254b496b5..3ecbef0d1f 100644
--- a/res/css/views/messages/_MessageActionBar.scss
+++ b/res/css/views/messages/_MessageActionBar.scss
@@ -105,3 +105,11 @@ limitations under the License.
.mx_MessageActionBar_optionsButton::after {
mask-image: url('$(res)/img/element-icons/context-menu.svg');
}
+
+.mx_MessageActionBar_resendButton::after {
+ mask-image: url('$(res)/img/element-icons/retry.svg');
+}
+
+.mx_MessageActionBar_cancelButton::after {
+ mask-image: url('$(res)/img/element-icons/trashcan.svg');
+}
diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss
index 2b3e179c54..5d1dd04383 100644
--- a/res/css/views/rooms/_EventTile.scss
+++ b/res/css/views/rooms/_EventTile.scss
@@ -214,10 +214,6 @@ $left-gutter: 64px;
color: $accent-fg-color;
}
-.mx_EventTile_notSent {
- 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
diff --git a/res/css/views/voip/_CallView.scss b/res/css/views/voip/_CallView.scss
index d13272c8c0..7292e325df 100644
--- a/res/css/views/voip/_CallView.scss
+++ b/res/css/views/voip/_CallView.scss
@@ -17,7 +17,7 @@ limitations under the License.
.mx_CallView {
border-radius: 8px;
- background-color: $voipcall-plinth-color;
+ background-color: $dark-panel-bg-color;
padding-left: 8px;
padding-right: 8px;
// XXX: CallContainer sets pointer-events: none - should probably be set back in a better place
@@ -40,7 +40,8 @@ limitations under the License.
width: 320px;
padding-bottom: 8px;
margin-top: 10px;
- box-shadow: 0px 14px 24px rgba(0, 0, 0, 0.08);
+ background-color: $voipcall-plinth-color;
+ box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.20);
border-radius: 8px;
.mx_CallView_voice {
diff --git a/res/img/element-icons/retry.svg b/res/img/element-icons/retry.svg
new file mode 100644
index 0000000000..09448d6458
--- /dev/null
+++ b/res/img/element-icons/retry.svg
@@ -0,0 +1,3 @@
+
diff --git a/res/img/element-icons/trashcan.svg b/res/img/element-icons/trashcan.svg
new file mode 100644
index 0000000000..f8fb8b5c46
--- /dev/null
+++ b/res/img/element-icons/trashcan.svg
@@ -0,0 +1,3 @@
+
diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss
index 925d268eb0..f9983dce9e 100644
--- a/res/themes/dark/css/_dark.scss
+++ b/res/themes/dark/css/_dark.scss
@@ -63,6 +63,8 @@ $input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: $bg-color;
+$resend-button-divider-color: #b9bec64a; // muted-text with a 4A opacity.
+
// scrollbars
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
$scrollbar-track-color: transparent;
@@ -110,7 +112,7 @@ $header-divider-color: $header-panel-text-primary-color;
$composer-e2e-icon-color: $header-panel-text-primary-color;
// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #21262c;
+$voipcall-plinth-color: #394049;
// ********************
diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss
index 28e6e22326..194e89e548 100644
--- a/res/themes/legacy-dark/css/_legacy-dark.scss
+++ b/res/themes/legacy-dark/css/_legacy-dark.scss
@@ -61,6 +61,8 @@ $input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: $bg-color;
+$resend-button-divider-color: $muted-fg-color;
+
// scrollbars
$scrollbar-thumb-color: rgba(255, 255, 255, 0.2);
$scrollbar-track-color: transparent;
@@ -107,7 +109,7 @@ $header-divider-color: $header-panel-text-primary-color;
$composer-e2e-icon-color: $header-panel-text-primary-color;
// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #f2f5f8;
+$voipcall-plinth-color: #394049;
// ********************
diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss
index 7b6bdad4a4..e05285721e 100644
--- a/res/themes/legacy-light/css/_legacy-light.scss
+++ b/res/themes/legacy-light/css/_legacy-light.scss
@@ -97,6 +97,8 @@ $input-invalid-border-color: $warning-color;
$field-focused-label-bg-color: #ffffff;
+$resend-button-divider-color: $input-darker-bg-color;
+
$button-bg-color: $accent-color;
$button-fg-color: white;
@@ -174,7 +176,7 @@ $composer-e2e-icon-color: #91a1c0;
$header-divider-color: #91a1c0;
// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #f2f5f8;
+$voipcall-plinth-color: #F4F6FA;
// ********************
diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss
index 5b46138dae..342b5dfd9a 100644
--- a/res/themes/light/css/_light.scss
+++ b/res/themes/light/css/_light.scss
@@ -91,6 +91,8 @@ $field-focused-label-bg-color: #ffffff;
$button-bg-color: $accent-color;
$button-fg-color: white;
+$resend-button-divider-color: $input-darker-bg-color;
+
// apart from login forms, which have stronger border
$strong-input-border-color: #c7c7c7;
@@ -165,7 +167,7 @@ $composer-e2e-icon-color: #91A1C0;
$header-divider-color: #91A1C0;
// this probably shouldn't have it's own colour
-$voipcall-plinth-color: #f2f5f8;
+$voipcall-plinth-color: #F4F6FA;
// ********************
diff --git a/scripts/compare-file.js b/scripts/compare-file.js
deleted file mode 100644
index f53275ebfa..0000000000
--- a/scripts/compare-file.js
+++ /dev/null
@@ -1,10 +0,0 @@
-const fs = require("fs");
-
-if (process.argv.length < 4) throw new Error("Missing source and target file arguments");
-
-const sourceFile = fs.readFileSync(process.argv[2], 'utf8');
-const targetFile = fs.readFileSync(process.argv[3], 'utf8');
-
-if (sourceFile !== targetFile) {
- throw new Error("Files do not match");
-}
diff --git a/scripts/gen-i18n.js b/scripts/gen-i18n.js
deleted file mode 100755
index 91733469f7..0000000000
--- a/scripts/gen-i18n.js
+++ /dev/null
@@ -1,304 +0,0 @@
-#!/usr/bin/env node
-
-/*
-Copyright 2017 New Vector Ltd
-
-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.
-*/
-
-/**
- * Regenerates the translations en_EN file by walking the source tree and
- * parsing each file with the appropriate parser. Emits a JSON file with the
- * translatable strings mapped to themselves in the order they appeared
- * in the files and grouped by the file they appeared in.
- *
- * Usage: node scripts/gen-i18n.js
- */
-const fs = require('fs');
-const path = require('path');
-
-const walk = require('walk');
-
-const parser = require("@babel/parser");
-const traverse = require("@babel/traverse");
-
-const TRANSLATIONS_FUNCS = ['_t', '_td'];
-
-const INPUT_TRANSLATIONS_FILE = 'src/i18n/strings/en_EN.json';
-const OUTPUT_FILE = 'src/i18n/strings/en_EN.json';
-
-// NB. The sync version of walk is broken for single files so we walk
-// all of res rather than just res/home.html.
-// https://git.daplie.com/Daplie/node-walk/merge_requests/1 fixes it,
-// or if we get bored waiting for it to be merged, we could switch
-// to a project that's actively maintained.
-const SEARCH_PATHS = ['src', 'res'];
-
-function getObjectValue(obj, key) {
- for (const prop of obj.properties) {
- if (prop.key.type === 'Identifier' && prop.key.name === key) {
- return prop.value;
- }
- }
- return null;
-}
-
-function getTKey(arg) {
- if (arg.type === 'Literal' || arg.type === "StringLiteral") {
- return arg.value;
- } else if (arg.type === 'BinaryExpression' && arg.operator === '+') {
- return getTKey(arg.left) + getTKey(arg.right);
- } else if (arg.type === 'TemplateLiteral') {
- return arg.quasis.map((q) => {
- return q.value.raw;
- }).join('');
- }
- return null;
-}
-
-function getFormatStrings(str) {
- // Match anything that starts with %
- // We could make a regex that matched the full placeholder, but this
- // would just not match invalid placeholders and so wouldn't help us
- // detect the invalid ones.
- // Also note that for simplicity, this just matches a % character and then
- // anything up to the next % character (or a single %, or end of string).
- const formatStringRe = /%([^%]+|%|$)/g;
- const formatStrings = new Set();
-
- let match;
- while ( (match = formatStringRe.exec(str)) !== null ) {
- const placeholder = match[1]; // Minus the leading '%'
- if (placeholder === '%') continue; // Literal % is %%
-
- const placeholderMatch = placeholder.match(/^\((.*?)\)(.)/);
- if (placeholderMatch === null) {
- throw new Error("Invalid format specifier: '"+match[0]+"'");
- }
- if (placeholderMatch.length < 3) {
- throw new Error("Malformed format specifier");
- }
- const placeholderName = placeholderMatch[1];
- const placeholderFormat = placeholderMatch[2];
-
- if (placeholderFormat !== 's') {
- throw new Error(`'${placeholderFormat}' used as format character: you probably meant 's'`);
- }
-
- formatStrings.add(placeholderName);
- }
-
- return formatStrings;
-}
-
-function getTranslationsJs(file) {
- const contents = fs.readFileSync(file, { encoding: 'utf8' });
-
- const trs = new Set();
-
- try {
- const plugins = [
- // https://babeljs.io/docs/en/babel-parser#plugins
- "classProperties",
- "objectRestSpread",
- "throwExpressions",
- "exportDefaultFrom",
- "decorators-legacy",
- ];
-
- if (file.endsWith(".js") || file.endsWith(".jsx")) {
- // all JS is assumed to be flow or react
- plugins.push("flow", "jsx");
- } else if (file.endsWith(".ts")) {
- // TS can't use JSX unless it's a TSX file (otherwise angle casts fail)
- plugins.push("typescript");
- } else if (file.endsWith(".tsx")) {
- // When the file is a TSX file though, enable JSX parsing
- plugins.push("typescript", "jsx");
- }
-
- const babelParsed = parser.parse(contents, {
- allowImportExportEverywhere: true,
- errorRecovery: true,
- sourceFilename: file,
- tokens: true,
- plugins,
- });
- traverse.default(babelParsed, {
- enter: (p) => {
- const node = p.node;
- if (p.isCallExpression() && node.callee && TRANSLATIONS_FUNCS.includes(node.callee.name)) {
- const tKey = getTKey(node.arguments[0]);
-
- // This happens whenever we call _t with non-literals (ie. whenever we've
- // had to use a _td to compensate) so is expected.
- if (tKey === null) return;
-
- // check the format string against the args
- // We only check _t: _td has no args
- if (node.callee.name === '_t') {
- try {
- const placeholders = getFormatStrings(tKey);
- for (const placeholder of placeholders) {
- if (node.arguments.length < 2) {
- throw new Error(`Placeholder found ('${placeholder}') but no substitutions given`);
- }
- const value = getObjectValue(node.arguments[1], placeholder);
- if (value === null) {
- throw new Error(`No value found for placeholder '${placeholder}'`);
- }
- }
-
- // Validate tag replacements
- if (node.arguments.length > 2) {
- const tagMap = node.arguments[2];
- for (const prop of tagMap.properties || []) {
- if (prop.key.type === 'Literal') {
- const tag = prop.key.value;
- // RegExp same as in src/languageHandler.js
- const regexp = new RegExp(`(<${tag}>(.*?)<\\/${tag}>|<${tag}>|<${tag}\\s*\\/>)`);
- if (!tKey.match(regexp)) {
- throw new Error(`No match for ${regexp} in ${tKey}`);
- }
- }
- }
- }
-
- } catch (e) {
- console.log();
- console.error(`ERROR: ${file}:${node.loc.start.line} ${tKey}`);
- console.error(e);
- process.exit(1);
- }
- }
-
- let isPlural = false;
- if (node.arguments.length > 1 && node.arguments[1].type === 'ObjectExpression') {
- const countVal = getObjectValue(node.arguments[1], 'count');
- if (countVal) {
- isPlural = true;
- }
- }
-
- if (isPlural) {
- trs.add(tKey + "|other");
- const plurals = enPlurals[tKey];
- if (plurals) {
- for (const pluralType of Object.keys(plurals)) {
- trs.add(tKey + "|" + pluralType);
- }
- }
- } else {
- trs.add(tKey);
- }
- }
- },
- });
- } catch (e) {
- console.error(e);
- process.exit(1);
- }
-
- return trs;
-}
-
-function getTranslationsOther(file) {
- const contents = fs.readFileSync(file, { encoding: 'utf8' });
-
- const trs = new Set();
-
- // Taken from element-web src/components/structures/HomePage.js
- const translationsRegex = /_t\(['"]([\s\S]*?)['"]\)/mg;
- let matches;
- while (matches = translationsRegex.exec(contents)) {
- trs.add(matches[1]);
- }
- return trs;
-}
-
-// gather en_EN plural strings from the input translations file:
-// the en_EN strings are all in the source with the exception of
-// pluralised strings, which we need to pull in from elsewhere.
-const inputTranslationsRaw = JSON.parse(fs.readFileSync(INPUT_TRANSLATIONS_FILE, { encoding: 'utf8' }));
-const enPlurals = {};
-
-for (const key of Object.keys(inputTranslationsRaw)) {
- const parts = key.split("|");
- if (parts.length > 1) {
- const plurals = enPlurals[parts[0]] || {};
- plurals[parts[1]] = inputTranslationsRaw[key];
- enPlurals[parts[0]] = plurals;
- }
-}
-
-const translatables = new Set();
-
-const walkOpts = {
- listeners: {
- names: function(root, nodeNamesArray) {
- // Sort the names case insensitively and alphabetically to
- // maintain some sense of order between the different strings.
- nodeNamesArray.sort((a, b) => {
- a = a.toLowerCase();
- b = b.toLowerCase();
- if (a > b) return 1;
- if (a < b) return -1;
- return 0;
- });
- },
- file: function(root, fileStats, next) {
- const fullPath = path.join(root, fileStats.name);
-
- let trs;
- if (fileStats.name.endsWith('.js') || fileStats.name.endsWith('.ts') || fileStats.name.endsWith('.tsx')) {
- trs = getTranslationsJs(fullPath);
- } else if (fileStats.name.endsWith('.html')) {
- trs = getTranslationsOther(fullPath);
- } else {
- return;
- }
- console.log(`${fullPath} (${trs.size} strings)`);
- for (const tr of trs.values()) {
- // Convert DOS line endings to unix
- translatables.add(tr.replace(/\r\n/g, "\n"));
- }
- },
- }
-};
-
-for (const path of SEARCH_PATHS) {
- if (fs.existsSync(path)) {
- walk.walkSync(path, walkOpts);
- }
-}
-
-const trObj = {};
-for (const tr of translatables) {
- if (tr.includes("|")) {
- if (inputTranslationsRaw[tr]) {
- trObj[tr] = inputTranslationsRaw[tr];
- } else {
- trObj[tr] = tr.split("|")[0];
- }
- } else {
- trObj[tr] = tr;
- }
-}
-
-fs.writeFileSync(
- OUTPUT_FILE,
- JSON.stringify(trObj, translatables.values(), 4) + "\n"
-);
-
-console.log();
-console.log(`Wrote ${translatables.size} strings to ${OUTPUT_FILE}`);
diff --git a/scripts/prune-i18n.js b/scripts/prune-i18n.js
deleted file mode 100755
index b4fe8d69f5..0000000000
--- a/scripts/prune-i18n.js
+++ /dev/null
@@ -1,68 +0,0 @@
-#!/usr/bin/env node
-
-/*
-Copyright 2017 New Vector Ltd
-
-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.
-*/
-
-/*
- * Looks through all the translation files and removes any strings
- * which don't appear in en_EN.json.
- * Use this if you remove a translation, but merge any outstanding changes
- * from weblate first or you'll need to resolve the conflict in weblate.
- */
-
-const fs = require('fs');
-const path = require('path');
-
-const I18NDIR = 'src/i18n/strings';
-
-const enStringsRaw = JSON.parse(fs.readFileSync(path.join(I18NDIR, 'en_EN.json')));
-
-const enStrings = new Set();
-for (const str of Object.keys(enStringsRaw)) {
- const parts = str.split('|');
- if (parts.length > 1) {
- enStrings.add(parts[0]);
- } else {
- enStrings.add(str);
- }
-}
-
-for (const filename of fs.readdirSync(I18NDIR)) {
- if (filename === 'en_EN.json') continue;
- if (filename === 'basefile.json') continue;
- if (!filename.endsWith('.json')) continue;
-
- const trs = JSON.parse(fs.readFileSync(path.join(I18NDIR, filename)));
- const oldLen = Object.keys(trs).length;
- for (const tr of Object.keys(trs)) {
- const parts = tr.split('|');
- const trKey = parts.length > 1 ? parts[0] : tr;
- if (!enStrings.has(trKey)) {
- delete trs[tr];
- }
- }
-
- const removed = oldLen - Object.keys(trs).length;
- if (removed > 0) {
- console.log(`${filename}: removed ${removed} translations`);
- // XXX: This is totally relying on the impl serialising the JSON object in the
- // same order as they were parsed from the file. JSON.stringify() has a specific argument
- // that can be used to control the order, but JSON.parse() lacks any kind of equivalent.
- // Empirically this does maintain the order on my system, so I'm going to leave it like
- // this for now.
- fs.writeFileSync(path.join(I18NDIR, filename), JSON.stringify(trs, undefined, 4) + "\n");
- }
-}
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 41257c21f0..01bb51732e 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -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");
you may not use this file except in compliance with the License.
@@ -40,6 +40,8 @@ import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import VoipUserMapper from "../VoipUserMapper";
import {SpaceStoreClass} from "../stores/SpaceStore";
import {VoiceRecording} from "../voice/VoiceRecording";
+import TypingStore from "../stores/TypingStore";
+import { EventIndexPeg } from "../indexing/EventIndexPeg";
declare global {
interface Window {
@@ -72,11 +74,15 @@ declare global {
mxVoipUserMapper: VoipUserMapper;
mxSpaceStore: SpaceStoreClass;
mxVoiceRecorder: typeof VoiceRecording;
+ mxTypingStore: TypingStore;
+ mxEventIndexPeg: EventIndexPeg;
}
interface Document {
// https://developer.mozilla.org/en-US/docs/Web/API/Document/hasStorageAccess
hasStorageAccess?: () => Promise;
+ // https://developer.mozilla.org/en-US/docs/Web/API/Document/requestStorageAccess
+ requestStorageAccess?: () => Promise;
// Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now
diff --git a/src/Avatar.ts b/src/Avatar.ts
index 76c88faa1c..d218ae8b46 100644
--- a/src/Avatar.ts
+++ b/src/Avatar.ts
@@ -27,11 +27,7 @@ export type ResizeMethod = "crop" | "scale";
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) {
let url: string;
if (member?.getMxcAvatarUrl()) {
- url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(
- Math.floor(width * window.devicePixelRatio),
- Math.floor(height * window.devicePixelRatio),
- resizeMethod,
- );
+ url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
if (!url) {
// member can be null here currently since on invites, the JS SDK
@@ -44,11 +40,7 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) {
if (!user.avatarUrl) return null;
- return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(
- Math.floor(width * window.devicePixelRatio),
- Math.floor(height * window.devicePixelRatio),
- resizeMethod,
- );
+ return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
}
function isValidHexColor(color: string): boolean {
diff --git a/src/BasePlatform.ts b/src/BasePlatform.ts
index b6012d7597..5483ea6874 100644
--- a/src/BasePlatform.ts
+++ b/src/BasePlatform.ts
@@ -258,7 +258,7 @@ export default abstract class BasePlatform {
return null;
}
- setLanguage(preferredLangs: string[]) {}
+ async setLanguage(preferredLangs: string[]) {}
setSpellCheckLanguages(preferredLangs: string[]) {}
diff --git a/src/CallHandler.tsx b/src/CallHandler.tsx
index be687a4474..237b323ce7 100644
--- a/src/CallHandler.tsx
+++ b/src/CallHandler.tsx
@@ -673,7 +673,7 @@ export default class CallHandler {
call.placeScreenSharingCall(
remoteElement,
localElement,
- async () : Promise => {
+ async (): Promise => {
const {finished} = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
return source;
diff --git a/src/KeyBindingsManager.ts b/src/KeyBindingsManager.ts
index d862f10c02..aac14bde20 100644
--- a/src/KeyBindingsManager.ts
+++ b/src/KeyBindingsManager.ts
@@ -231,8 +231,10 @@ export class KeyBindingsManager {
/**
* Finds a matching KeyAction for a given KeyboardEvent
*/
- private getAction(getters: KeyBindingGetter[], ev: KeyboardEvent | React.KeyboardEvent)
- : T | undefined {
+ private getAction(
+ getters: KeyBindingGetter[],
+ ev: KeyboardEvent | React.KeyboardEvent,
+ ): T | undefined {
for (const getter of getters) {
const bindings = getter();
const binding = bindings.find(it => isKeyComboMatch(ev, it.keyCombo, isMac));
diff --git a/src/Login.ts b/src/Login.ts
index db3c4c11e4..d584df7dfe 100644
--- a/src/Login.ts
+++ b/src/Login.ts
@@ -1,9 +1,6 @@
/*
-Copyright 2015, 2016 OpenMarket Ltd
-Copyright 2017 Vector Creations Ltd
-Copyright 2018 New Vector Ltd
+Copyright 2015-2021 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
-Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -59,7 +56,7 @@ export type LoginFlow = ISSOFlow | IPasswordFlow;
// TODO: Move this to JS SDK
/* eslint-disable camelcase */
interface ILoginParams {
- identifier?: string;
+ identifier?: object;
password?: string;
token?: string;
device_id?: string;
diff --git a/src/Resend.js b/src/Resend.js
index bf69e59c1a..f1e5fb38f5 100644
--- a/src/Resend.js
+++ b/src/Resend.js
@@ -21,11 +21,11 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event';
export default class Resend {
static resendUnsentEvents(room) {
- room.getPendingEvents().filter(function(ev) {
+ return Promise.all(room.getPendingEvents().filter(function(ev) {
return ev.status === EventStatus.NOT_SENT;
- }).forEach(function(event) {
- Resend.resend(event);
- });
+ }).map(function(event) {
+ return Resend.resend(event);
+ }));
}
static cancelUnsentEvents(room) {
@@ -38,7 +38,7 @@ export default class Resend {
static resend(event) {
const room = MatrixClientPeg.get().getRoom(event.getRoomId());
- MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
+ return MatrixClientPeg.get().resendEvent(event, room).then(function(res) {
dis.dispatch({
action: 'message_sent',
event: event,
diff --git a/src/ScalarAuthClient.js b/src/ScalarAuthClient.ts
similarity index 84%
rename from src/ScalarAuthClient.js
rename to src/ScalarAuthClient.ts
index 200b4fd7b9..a09c3494a8 100644
--- a/src/ScalarAuthClient.js
+++ b/src/ScalarAuthClient.ts
@@ -1,6 +1,5 @@
/*
-Copyright 2016 OpenMarket Ltd
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 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.
@@ -17,13 +16,14 @@ limitations under the License.
import url from 'url';
import SettingsStore from "./settings/SettingsStore";
-import { Service, startTermsFlow, TermsNotSignedError } from './Terms';
+import { Service, startTermsFlow, TermsInteractionCallback, TermsNotSignedError } from './Terms';
import {MatrixClientPeg} from "./MatrixClientPeg";
import request from "browser-request";
import SdkConfig from "./SdkConfig";
import {WidgetType} from "./widgets/WidgetType";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types";
+import { Room } from "matrix-js-sdk/src/models/room";
// The version of the integration manager API we're intending to work with
const imApiVersion = "1.1";
@@ -31,9 +31,11 @@ const imApiVersion = "1.1";
// TODO: Generify the name of this class and all components within - it's not just for Scalar.
export default class ScalarAuthClient {
- constructor(apiUrl, uiUrl) {
- this.apiUrl = apiUrl;
- this.uiUrl = uiUrl;
+ private scalarToken: string;
+ private termsInteractionCallback: TermsInteractionCallback;
+ private isDefaultManager: boolean;
+
+ constructor(private apiUrl: string, private uiUrl: string) {
this.scalarToken = null;
// `undefined` to allow `startTermsFlow` to fallback to a default
// callback if this is unset.
@@ -46,7 +48,7 @@ export default class ScalarAuthClient {
this.isDefaultManager = apiUrl === configApiUrl && configUiUrl === uiUrl;
}
- _writeTokenToStore() {
+ private writeTokenToStore() {
window.localStorage.setItem("mx_scalar_token_at_" + this.apiUrl, this.scalarToken);
if (this.isDefaultManager) {
// We remove the old token from storage to migrate upwards. This is safe
@@ -56,7 +58,7 @@ export default class ScalarAuthClient {
}
}
- _readTokenFromStore() {
+ private readTokenFromStore(): string {
let token = window.localStorage.getItem("mx_scalar_token_at_" + this.apiUrl);
if (!token && this.isDefaultManager) {
token = window.localStorage.getItem("mx_scalar_token");
@@ -64,33 +66,33 @@ export default class ScalarAuthClient {
return token;
}
- _readToken() {
+ private readToken(): string {
if (this.scalarToken) return this.scalarToken;
- return this._readTokenFromStore();
+ return this.readTokenFromStore();
}
setTermsInteractionCallback(callback) {
this.termsInteractionCallback = callback;
}
- connect() {
+ connect(): Promise {
return this.getScalarToken().then((tok) => {
this.scalarToken = tok;
});
}
- hasCredentials() {
+ hasCredentials(): boolean {
return this.scalarToken != null; // undef or null
}
// Returns a promise that resolves to a scalar_token string
- getScalarToken() {
- const token = this._readToken();
+ getScalarToken(): Promise {
+ const token = this.readToken();
if (!token) {
return this.registerForToken();
} else {
- return this._checkToken(token).catch((e) => {
+ return this.checkToken(token).catch((e) => {
if (e instanceof TermsNotSignedError) {
// retrying won't help this
throw e;
@@ -100,7 +102,7 @@ export default class ScalarAuthClient {
}
}
- _getAccountName(token) {
+ private getAccountName(token: string): Promise {
const url = this.apiUrl + "/account";
return new Promise(function(resolve, reject) {
@@ -125,8 +127,8 @@ export default class ScalarAuthClient {
});
}
- _checkToken(token) {
- return this._getAccountName(token).then(userId => {
+ private checkToken(token: string): Promise {
+ return this.getAccountName(token).then(userId => {
const me = MatrixClientPeg.get().getUserId();
if (userId !== me) {
throw new Error("Scalar token is owned by someone else: " + me);
@@ -154,7 +156,7 @@ export default class ScalarAuthClient {
parsedImRestUrl.pathname = '';
return startTermsFlow([new Service(
SERVICE_TYPES.IM,
- parsedImRestUrl.format(),
+ url.format(parsedImRestUrl),
token,
)], this.termsInteractionCallback).then(() => {
return token;
@@ -165,22 +167,22 @@ export default class ScalarAuthClient {
});
}
- registerForToken() {
+ registerForToken(): Promise {
// Get openid bearer token from the HS as the first part of our dance
return MatrixClientPeg.get().getOpenIdToken().then((tokenObject) => {
// Now we can send that to scalar and exchange it for a scalar token
return this.exchangeForScalarToken(tokenObject);
}).then((token) => {
// Validate it (this mostly checks to see if the IM needs us to agree to some terms)
- return this._checkToken(token);
+ return this.checkToken(token);
}).then((token) => {
this.scalarToken = token;
- this._writeTokenToStore();
+ this.writeTokenToStore();
return token;
});
}
- exchangeForScalarToken(openidTokenObject) {
+ exchangeForScalarToken(openidTokenObject: any): Promise {
const scalarRestUrl = this.apiUrl;
return new Promise(function(resolve, reject) {
@@ -194,7 +196,7 @@ export default class ScalarAuthClient {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
- reject({statusCode: response.statusCode});
+ reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body || !body.scalar_token) {
reject(new Error("Missing scalar_token in response"));
} else {
@@ -204,7 +206,7 @@ export default class ScalarAuthClient {
});
}
- getScalarPageTitle(url) {
+ getScalarPageTitle(url: string): Promise {
let scalarPageLookupUrl = this.apiUrl + '/widgets/title_lookup';
scalarPageLookupUrl = this.getStarterLink(scalarPageLookupUrl);
scalarPageLookupUrl += '&curl=' + encodeURIComponent(url);
@@ -218,7 +220,7 @@ export default class ScalarAuthClient {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
- reject({statusCode: response.statusCode});
+ reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body) {
reject(new Error("Missing page title in response"));
} else {
@@ -240,10 +242,10 @@ export default class ScalarAuthClient {
* @param {string} widgetId The widget ID to disable assets for
* @return {Promise} Resolves on completion
*/
- disableWidgetAssets(widgetType: WidgetType, widgetId) {
+ disableWidgetAssets(widgetType: WidgetType, widgetId: string): Promise {
let url = this.apiUrl + '/widgets/set_assets_state';
url = this.getStarterLink(url);
- return new Promise((resolve, reject) => {
+ return new Promise((resolve, reject) => {
request({
method: 'GET', // XXX: Actions shouldn't be GET requests
uri: url,
@@ -257,7 +259,7 @@ export default class ScalarAuthClient {
if (err) {
reject(err);
} else if (response.statusCode / 100 !== 2) {
- reject({statusCode: response.statusCode});
+ reject(new Error(`Scalar request failed: ${response.statusCode}`));
} else if (!body) {
reject(new Error("Failed to set widget assets state"));
} else {
@@ -267,7 +269,7 @@ export default class ScalarAuthClient {
});
}
- getScalarInterfaceUrlForRoom(room, screen, id) {
+ getScalarInterfaceUrlForRoom(room: Room, screen: string, id: string): string {
const roomId = room.roomId;
const roomName = room.name;
let url = this.uiUrl;
@@ -284,7 +286,7 @@ export default class ScalarAuthClient {
return url;
}
- getStarterLink(starterLinkUrl) {
+ getStarterLink(starterLinkUrl: string): string {
return starterLinkUrl + "?scalar_token=" + encodeURIComponent(this.scalarToken);
}
}
diff --git a/src/Terms.js b/src/Terms.ts
similarity index 87%
rename from src/Terms.js
rename to src/Terms.ts
index 6ae89f9a2c..1bdff36cbc 100644
--- a/src/Terms.js
+++ b/src/Terms.ts
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 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.
@@ -17,7 +17,7 @@ limitations under the License.
import classNames from 'classnames';
import {MatrixClientPeg} from './MatrixClientPeg';
-import * as sdk from './';
+import * as sdk from '.';
import Modal from './Modal';
export class TermsNotSignedError extends Error {}
@@ -32,13 +32,30 @@ export class Service {
* @param {string} baseUrl The Base URL of the service (ie. before '/_matrix')
* @param {string} accessToken The user's access token for the service
*/
- constructor(serviceType, baseUrl, accessToken) {
- this.serviceType = serviceType;
- this.baseUrl = baseUrl;
- this.accessToken = accessToken;
+ constructor(public serviceType: string, public baseUrl: string, public accessToken: string) {
}
}
+interface Policy {
+ // @ts-ignore: No great way to express indexed types together with other keys
+ version: string;
+ [lang: string]: {
+ url: string;
+ };
+}
+type Policies = {
+ [policy: string]: Policy,
+};
+
+export type TermsInteractionCallback = (
+ policiesAndServicePairs: {
+ service: Service,
+ policies: Policies,
+ }[],
+ agreedUrls: string[],
+ extraClassNames?: string,
+) => Promise;
+
/**
* Start a flow where the user is presented with terms & conditions for some services
*
@@ -51,8 +68,8 @@ export class Service {
* if they cancel.
*/
export async function startTermsFlow(
- services,
- interactionCallback = dialogTermsInteractionCallback,
+ services: Service[],
+ interactionCallback: TermsInteractionCallback = dialogTermsInteractionCallback,
) {
const termsPromises = services.map(
(s) => MatrixClientPeg.get().getTerms(s.serviceType, s.baseUrl),
@@ -77,7 +94,7 @@ export async function startTermsFlow(
* }
*/
- const terms = await Promise.all(termsPromises);
+ const terms: { policies: Policies }[] = await Promise.all(termsPromises);
const policiesAndServicePairs = terms.map((t, i) => { return { 'service': services[i], 'policies': t.policies }; });
// fetch the set of agreed policy URLs from account data
@@ -158,10 +175,13 @@ export async function startTermsFlow(
}
export function dialogTermsInteractionCallback(
- policiesAndServicePairs,
- agreedUrls,
- extraClassNames,
-) {
+ policiesAndServicePairs: {
+ service: Service,
+ policies: { [policy: string]: Policy },
+ }[],
+ agreedUrls: string[],
+ extraClassNames?: string,
+): Promise {
return new Promise((resolve, reject) => {
console.log("Terms that need agreement", policiesAndServicePairs);
const TermsDialog = sdk.getComponent("views.dialogs.TermsDialog");
diff --git a/src/Unread.js b/src/Unread.js
index ddf225ac64..12c15eb6af 100644
--- a/src/Unread.js
+++ b/src/Unread.js
@@ -45,7 +45,7 @@ export function eventTriggersUnreadCount(ev) {
}
export function doesRoomHaveUnreadMessages(room) {
- const myUserId = MatrixClientPeg.get().credentials.userId;
+ const myUserId = MatrixClientPeg.get().getUserId();
// get the most recent read receipt sent by our account.
// N.B. this is NOT a read marker (RM, aka "read up to marker"),
diff --git a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
similarity index 90%
rename from src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js
rename to src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
index be3368b87b..0710c513da 100644
--- a/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.js
+++ b/src/async-components/views/dialogs/eventindex/ManageEventIndexDialog.tsx
@@ -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");
you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@ limitations under the License.
import React from 'react';
import * as sdk from '../../../../index';
-import PropTypes from 'prop-types';
import { _t } from '../../../../languageHandler';
import SdkConfig from '../../../../SdkConfig';
import SettingsStore from "../../../../settings/SettingsStore";
@@ -26,14 +25,23 @@ import {formatBytes, formatCountLong} from "../../../../utils/FormattingUtils";
import EventIndexPeg from "../../../../indexing/EventIndexPeg";
import {SettingLevel} from "../../../../settings/SettingLevel";
+interface IProps {
+ onFinished: (confirmed: boolean) => void;
+}
+
+interface IState {
+ eventIndexSize: number;
+ eventCount: number;
+ crawlingRoomsCount: number;
+ roomCount: number;
+ currentRoom: string;
+ crawlerSleepTime: number;
+}
+
/*
* Allows the user to introspect the event index state and disable it.
*/
-export default class ManageEventIndexDialog extends React.Component {
- static propTypes = {
- onFinished: PropTypes.func.isRequired,
- };
-
+export default class ManageEventIndexDialog extends React.Component {
constructor(props) {
super(props);
@@ -84,7 +92,7 @@ export default class ManageEventIndexDialog extends React.Component {
}
}
- async componentDidMount(): void {
+ async componentDidMount(): Promise {
let eventIndexSize = 0;
let crawlingRoomsCount = 0;
let roomCount = 0;
@@ -123,14 +131,14 @@ export default class ManageEventIndexDialog extends React.Component {
});
}
- _onDisable = async () => {
+ private onDisable = async () => {
Modal.createTrackedDialogAsync("Disable message search", "Disable message search",
import("./DisableEventIndexDialog"),
null, null, /* priority = */ false, /* static = */ true,
);
};
- _onCrawlerSleepTimeChange = (e) => {
+ private onCrawlerSleepTimeChange = (e) => {
this.setState({crawlerSleepTime: e.target.value});
SettingsStore.setValue("crawlerSleepTime", null, SettingLevel.DEVICE, e.target.value);
};
@@ -144,7 +152,7 @@ export default class ManageEventIndexDialog extends React.Component {
crawlerState = _t("Not currently indexing messages for any room.");
} else {
crawlerState = (
- _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom })
+ _t("Currently indexing: %(currentRoom)s", { currentRoom: this.state.currentRoom })
);
}
@@ -169,7 +177,7 @@ export default class ManageEventIndexDialog extends React.Component {
label={_t('Message downloading sleep time(ms)')}
type='number'
value={this.state.crawlerSleepTime}
- onChange={this._onCrawlerSleepTimeChange} />
+ onChange={this.onCrawlerSleepTimeChange} />
);
@@ -188,7 +196,7 @@ export default class ManageEventIndexDialog extends React.Component {
onPrimaryButtonClick={this.props.onFinished}
primaryButtonClass="primary"
cancelButton={_t("Disable")}
- onCancel={this._onDisable}
+ onCancel={this.onDisable}
cancelButtonClass="danger"
/>
diff --git a/src/components/structures/RoomStatusBar.js b/src/components/structures/RoomStatusBar.js
index 54b6fee233..38e3cd97e8 100644
--- a/src/components/structures/RoomStatusBar.js
+++ b/src/components/structures/RoomStatusBar.js
@@ -1,5 +1,5 @@
/*
-Copyright 2015-2020 The Matrix.org Foundation C.I.C.
+Copyright 2015-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.
@@ -20,16 +20,20 @@ import { _t, _td } from '../../languageHandler';
import {MatrixClientPeg} from '../../MatrixClientPeg';
import Resend from '../../Resend';
import dis from '../../dispatcher/dispatcher';
-import {messageForResourceLimitError, messageForSendError} from '../../utils/ErrorUtils';
+import {messageForResourceLimitError} from '../../utils/ErrorUtils';
import {Action} from "../../dispatcher/actions";
import {replaceableComponent} from "../../utils/replaceableComponent";
import {EventStatus} from "matrix-js-sdk/src/models/event";
+import NotificationBadge from "../views/rooms/NotificationBadge";
+import {StaticNotificationState} from "../../stores/notifications/StaticNotificationState";
+import AccessibleButton from "../views/elements/AccessibleButton";
+import InlineSpinner from "../views/elements/InlineSpinner";
const STATUS_BAR_HIDDEN = 0;
const STATUS_BAR_EXPANDED = 1;
const STATUS_BAR_EXPANDED_LARGE = 2;
-function getUnsentMessages(room) {
+export function getUnsentMessages(room) {
if (!room) { return []; }
return room.getPendingEvents().filter(function(ev) {
return ev.status === EventStatus.NOT_SENT;
@@ -76,6 +80,7 @@ export default class RoomStatusBar extends React.Component {
syncState: MatrixClientPeg.get().getSyncState(),
syncStateData: MatrixClientPeg.get().getSyncStateData(),
unsentMessages: getUnsentMessages(this.props.room),
+ isResending: false,
};
componentDidMount() {
@@ -109,7 +114,10 @@ export default class RoomStatusBar extends React.Component {
};
_onResendAllClick = () => {
- Resend.resendUnsentEvents(this.props.room);
+ Resend.resendUnsentEvents(this.props.room).then(() => {
+ this.setState({isResending: false});
+ });
+ this.setState({isResending: true});
dis.fire(Action.FocusComposer);
};
@@ -120,9 +128,10 @@ export default class RoomStatusBar extends React.Component {
_onRoomLocalEchoUpdated = (event, room, oldEventId, oldStatus) => {
if (room.roomId !== this.props.room.roomId) return;
-
+ const messages = getUnsentMessages(this.props.room);
this.setState({
- unsentMessages: getUnsentMessages(this.props.room),
+ unsentMessages: messages,
+ isResending: messages.length > 0 && this.state.isResending,
});
};
@@ -141,7 +150,7 @@ export default class RoomStatusBar extends React.Component {
_getSize() {
if (this._shouldShowConnectionError()) {
return STATUS_BAR_EXPANDED;
- } else if (this.state.unsentMessages.length > 0) {
+ } else if (this.state.unsentMessages.length > 0 || this.state.isResending) {
return STATUS_BAR_EXPANDED_LARGE;
}
return STATUS_BAR_HIDDEN;
@@ -162,7 +171,6 @@ export default class RoomStatusBar extends React.Component {
_getUnsentMessageContent() {
const unsentMessages = this.state.unsentMessages;
- if (!unsentMessages.length) return null;
let title;
@@ -206,75 +214,76 @@ export default class RoomStatusBar extends React.Component {
"Please contact your service administrator to continue using the service.",
),
});
- } else if (
- unsentMessages.length === 1 &&
- unsentMessages[0].error &&
- unsentMessages[0].error.data &&
- unsentMessages[0].error.data.error
- ) {
- title = messageForSendError(unsentMessages[0].error.data) || unsentMessages[0].error.data.error;
} else {
- title = _t('%(count)s of your messages have not been sent.', { count: unsentMessages.length });
+ title = _t('Some of your messages have not been sent');
}
- const content = _t("%(count)s Resend all or cancel all " +
- "now. You can also select individual messages to resend or cancel.",
- { count: unsentMessages.length },
- {
- 'resendText': (sub) =>
- { sub },
- 'cancelText': (sub) =>
- { sub },
- },
- );
+ let buttonRow = <>
+
+ {_t("Delete all")}
+
+
+ {_t("Retry all")}
+
+ >;
+ if (this.state.isResending) {
+ buttonRow = <>
+
+ {/* span for css */}
+ {_t("Sending")}
+ >;
+ }
- return
-
-
-
- { title }
-
-
- { content }
+ return <>
+
+
+
+
+
+
+
+ { title }
+
+
+ { _t("You can select all or individual messages to retry or delete") }
+
+
+
+ {buttonRow}
+
-
;
+ >;
}
- // return suitable content for the main (text) part of the status bar.
- _getContent() {
+ render() {
if (this._shouldShowConnectionError()) {
return (
-
-
-
-
- { _t('Connectivity to the server has been lost.') }
-
-
- { _t('Sent messages will be stored until your connection has returned.') }
+
+
+
+
+
+
+ {_t('Connectivity to the server has been lost.')}
+
+
+ {_t('Sent messages will be stored until your connection has returned.')}
+
@@ -1235,8 +1277,8 @@ interface ISentReceiptState {
}
class SentReceipt extends React.PureComponent {
- constructor() {
- super();
+ constructor(props) {
+ super(props);
this.state = {
hover: false,
@@ -1253,11 +1295,19 @@ class SentReceipt extends React.PureComponent;
+ }
+
let tooltip = null;
if (this.state.hover) {
let label = _t("Sending your message...");
@@ -1265,6 +1315,8 @@ class SentReceipt extends React.PureComponent
+ {nonCssBadge}
{tooltip}
;
diff --git a/src/components/views/rooms/MessageComposer.js b/src/components/views/rooms/MessageComposer.tsx
similarity index 83%
rename from src/components/views/rooms/MessageComposer.js
rename to src/components/views/rooms/MessageComposer.tsx
index 5178d52305..d126a7b161 100644
--- a/src/components/views/rooms/MessageComposer.js
+++ b/src/components/views/rooms/MessageComposer.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2015-2018, 2020, 2021 The Matrix.org Foundation C.I.C.
+Copyright 2015-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.
@@ -13,15 +13,18 @@ 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, {createRef} from 'react';
+import React from 'react';
import classNames from 'classnames';
-import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg';
import * as sdk from '../../../index';
+import {MatrixEvent} from "matrix-js-sdk/src/models/event";
+import {Room} from "matrix-js-sdk/src/models/room";
+import {RoomMember} from "matrix-js-sdk/src/models/room-member";
import dis from '../../../dispatcher/dispatcher';
+import { ActionPayload } from "../../../dispatcher/payloads";
import Stickerpicker from './Stickerpicker';
-import { makeRoomPermalink } from '../../../utils/permalinks/Permalinks';
+import { makeRoomPermalink, RoomPermalinkCreator } from '../../../utils/permalinks/Permalinks';
import ContentMessages from '../../../ContentMessages';
import E2EIcon from './E2EIcon';
import SettingsStore from "../../../settings/SettingsStore";
@@ -35,19 +38,26 @@ import VoiceRecordComposerTile from "./VoiceRecordComposerTile";
import {VoiceRecordingStore} from "../../../stores/VoiceRecordingStore";
import {RecordingState} from "../../../voice/VoiceRecording";
import Tooltip, {Alignment} from "../elements/Tooltip";
+import ResizeNotifier from "../../../utils/ResizeNotifier";
+import { E2EStatus } from '../../../utils/ShieldUtils';
+import SendMessageComposer from "./SendMessageComposer";
-function ComposerAvatar(props) {
+interface IComposerAvatarProps {
+ me: object;
+}
+
+function ComposerAvatar(props: IComposerAvatarProps) {
const MemberStatusMessageAvatar = sdk.getComponent('avatars.MemberStatusMessageAvatar');
return
);
}
diff --git a/src/components/views/settings/SetIdServer.js b/src/components/views/settings/SetIdServer.tsx
similarity index 89%
rename from src/components/views/settings/SetIdServer.js
rename to src/components/views/settings/SetIdServer.tsx
index fa2a36476d..05d1f83387 100644
--- a/src/components/views/settings/SetIdServer.js
+++ b/src/components/views/settings/SetIdServer.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019 The Matrix.org Foundation C.I.C.
+Copyright 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.
@@ -16,7 +16,6 @@ limitations under the License.
import url from 'url';
import React from 'react';
-import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler";
import * as sdk from '../../../index';
import {MatrixClientPeg} from "../../../MatrixClientPeg";
@@ -28,6 +27,7 @@ import {abbreviateUrl, unabbreviateUrl} from "../../../utils/UrlUtils";
import { getDefaultIdentityServerUrl, doesIdentityServerHaveTerms } from '../../../utils/IdentityServerUtils';
import {timeout} from "../../../utils/promise";
import {replaceableComponent} from "../../../utils/replaceableComponent";
+import { ActionPayload } from '../../../dispatcher/payloads';
// We'll wait up to this long when checking for 3PID bindings on the IS.
const REACHABILITY_TIMEOUT = 10000; // ms
@@ -59,16 +59,28 @@ async function checkIdentityServerUrl(u) {
}
}
-@replaceableComponent("views.settings.SetIdServer")
-export default class SetIdServer extends React.Component {
- static propTypes = {
- // Whether or not the ID server is missing terms. This affects the text
- // shown to the user.
- missingTerms: PropTypes.bool,
- };
+interface IProps {
+ // Whether or not the ID server is missing terms. This affects the text
+ // shown to the user.
+ missingTerms: boolean;
+}
- constructor() {
- super();
+interface IState {
+ defaultIdServer?: string;
+ currentClientIdServer: string;
+ idServer?: string;
+ error?: string;
+ busy: boolean;
+ disconnectBusy: boolean;
+ checking: boolean;
+}
+
+@replaceableComponent("views.settings.SetIdServer")
+export default class SetIdServer extends React.Component {
+ private dispatcherRef: string;
+
+ constructor(props) {
+ super(props);
let defaultIdServer = '';
if (!MatrixClientPeg.get().getIdentityServerUrl() && getDefaultIdentityServerUrl()) {
@@ -96,7 +108,7 @@ export default class SetIdServer extends React.Component {
dis.unregister(this.dispatcherRef);
}
- onAction = (payload) => {
+ private onAction = (payload: ActionPayload) => {
// We react to changes in the ID server in the event the user is staring at this form
// when changing their identity server on another device.
if (payload.action !== "id_server_changed") return;
@@ -106,13 +118,13 @@ export default class SetIdServer extends React.Component {
});
};
- _onIdentityServerChanged = (ev) => {
+ private onIdentityServerChanged = (ev) => {
const u = ev.target.value;
this.setState({idServer: u});
};
- _getTooltip = () => {
+ private getTooltip = () => {
if (this.state.checking) {
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
return
@@ -126,11 +138,11 @@ export default class SetIdServer extends React.Component {
}
};
- _idServerChangeEnabled = () => {
+ private idServerChangeEnabled = () => {
return !!this.state.idServer && !this.state.busy;
};
- _saveIdServer = (fullUrl) => {
+ private saveIdServer = (fullUrl) => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: fullUrl,
@@ -143,7 +155,7 @@ export default class SetIdServer extends React.Component {
});
};
- _checkIdServer = async (e) => {
+ private checkIdServer = async (e) => {
e.preventDefault();
const { idServer, currentClientIdServer } = this.state;
@@ -166,14 +178,14 @@ export default class SetIdServer extends React.Component {
// Double check that the identity server even has terms of service.
const hasTerms = await doesIdentityServerHaveTerms(fullUrl);
if (!hasTerms) {
- const [confirmed] = await this._showNoTermsWarning(fullUrl);
+ const [confirmed] = await this.showNoTermsWarning(fullUrl);
save = confirmed;
}
// Show a general warning, possibly with details about any bound
// 3PIDs that would be left behind.
if (save && currentClientIdServer && fullUrl !== currentClientIdServer) {
- const [confirmed] = await this._showServerChangeWarning({
+ const [confirmed] = await this.showServerChangeWarning({
title: _t("Change identity server"),
unboundMessage: _t(
"Disconnect from the identity server and " +
@@ -189,7 +201,7 @@ export default class SetIdServer extends React.Component {
}
if (save) {
- this._saveIdServer(fullUrl);
+ this.saveIdServer(fullUrl);
}
} catch (e) {
console.error(e);
@@ -204,7 +216,7 @@ export default class SetIdServer extends React.Component {
});
};
- _showNoTermsWarning(fullUrl) {
+ private showNoTermsWarning(fullUrl) {
const QuestionDialog = sdk.getComponent("views.dialogs.QuestionDialog");
const { finished } = Modal.createTrackedDialog('No Terms Warning', '', QuestionDialog, {
title: _t("Identity server has no terms of service"),
@@ -223,10 +235,10 @@ export default class SetIdServer extends React.Component {
return finished;
}
- _onDisconnectClicked = async () => {
+ private onDisconnectClicked = async () => {
this.setState({disconnectBusy: true});
try {
- const [confirmed] = await this._showServerChangeWarning({
+ const [confirmed] = await this.showServerChangeWarning({
title: _t("Disconnect identity server"),
unboundMessage: _t(
"Disconnect from the identity server ?", {},
@@ -235,14 +247,14 @@ export default class SetIdServer extends React.Component {
button: _t("Disconnect"),
});
if (confirmed) {
- this._disconnectIdServer();
+ this.disconnectIdServer();
}
} finally {
this.setState({disconnectBusy: false});
}
};
- async _showServerChangeWarning({ title, unboundMessage, button }) {
+ private async showServerChangeWarning({ title, unboundMessage, button }) {
const { currentClientIdServer } = this.state;
let threepids = [];
@@ -318,7 +330,7 @@ export default class SetIdServer extends React.Component {
return finished;
}
- _disconnectIdServer = () => {
+ private disconnectIdServer = () => {
// Account data change will update localstorage, client, etc through dispatcher
MatrixClientPeg.get().setAccountData("m.identity_server", {
base_url: null, // clear
@@ -371,7 +383,7 @@ export default class SetIdServer extends React.Component {
let discoSection;
if (idServerUrl) {
- let discoButtonContent = _t("Disconnect");
+ let discoButtonContent: React.ReactNode = _t("Disconnect");
let discoBodyText = _t(
"Disconnecting from your identity server will mean you " +
"won't be discoverable by other users and you won't be " +
@@ -391,14 +403,14 @@ export default class SetIdServer extends React.Component {
}
discoSection =
{discoBodyText}
-
+
{discoButtonContent}
;
}
return (
-
diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
similarity index 89%
rename from src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
rename to src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
index 59a175906d..4fa521f598 100644
--- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.js
+++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx
@@ -1,5 +1,5 @@
/*
-Copyright 2019, 2021 The Matrix.org Foundation C.I.C.
+Copyright 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.
@@ -15,7 +15,6 @@ limitations under the License.
*/
import React from 'react';
-import PropTypes from 'prop-types';
import {_t, _td} from "../../../../../languageHandler";
import {MatrixClientPeg} from "../../../../../MatrixClientPeg";
import * as sdk from "../../../../..";
@@ -23,6 +22,9 @@ import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
import {replaceableComponent} from "../../../../../utils/replaceableComponent";
import {EventType} from "matrix-js-sdk/src/@types/event";
+import { RoomMember } from "matrix-js-sdk/src/models/room-member";
+import { MatrixEvent } from "matrix-js-sdk/src/models/event";
+import { RoomState } from "matrix-js-sdk/src/models/room-state";
const plEventsToLabels = {
// These will be translated for us later.
@@ -63,15 +65,15 @@ function parseIntWithDefault(val, def) {
return isNaN(res) ? def : res;
}
-export class BannedUser extends React.Component {
- static propTypes = {
- canUnban: PropTypes.bool,
- member: PropTypes.object.isRequired, // js-sdk RoomMember
- by: PropTypes.string.isRequired,
- reason: PropTypes.string,
- };
+interface IBannedUserProps {
+ canUnban?: boolean;
+ member: RoomMember;
+ by: string;
+ reason?: string;
+}
- _onUnbanClick = (e) => {
+export class BannedUser extends React.Component {
+ private onUnbanClick = (e) => {
MatrixClientPeg.get().unban(this.props.member.roomId, this.props.member.userId).catch((err) => {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to unban: " + err);
@@ -87,8 +89,10 @@ export class BannedUser extends React.Component {
if (this.props.canUnban) {
unbanButton = (
-
+
{ _t('Unban') }
);
@@ -107,29 +111,29 @@ export class BannedUser extends React.Component {
}
}
-@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab")
-export default class RolesRoomSettingsTab extends React.Component {
- static propTypes = {
- roomId: PropTypes.string.isRequired,
- };
+interface IProps {
+ roomId: string;
+}
- componentDidMount(): void {
- MatrixClientPeg.get().on("RoomState.members", this._onRoomMembership);
+@replaceableComponent("views.settings.tabs.room.RolesRoomSettingsTab")
+export default class RolesRoomSettingsTab extends React.Component {
+ componentDidMount() {
+ MatrixClientPeg.get().on("RoomState.members", this.onRoomMembership);
}
- componentWillUnmount(): void {
+ componentWillUnmount() {
const client = MatrixClientPeg.get();
if (client) {
- client.removeListener("RoomState.members", this._onRoomMembership);
+ client.removeListener("RoomState.members", this.onRoomMembership);
}
}
- _onRoomMembership = (event, state, member) => {
+ private onRoomMembership = (event: MatrixEvent, state: RoomState, member: RoomMember) => {
if (state.roomId !== this.props.roomId) return;
this.forceUpdate();
};
- _populateDefaultPlEvents(eventsSection, stateLevel, eventsLevel) {
+ private populateDefaultPlEvents(eventsSection: Record, stateLevel: number, eventsLevel: number) {
for (const desiredEvent of Object.keys(plEventsToShow)) {
if (!(desiredEvent in eventsSection)) {
eventsSection[desiredEvent] = (plEventsToShow[desiredEvent].isState ? stateLevel : eventsLevel);
@@ -137,7 +141,7 @@ export default class RolesRoomSettingsTab extends React.Component {
}
}
- _onPowerLevelsChanged = (value, powerLevelKey) => {
+ private onPowerLevelsChanged = (inputValue: string, powerLevelKey: string) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
@@ -148,7 +152,7 @@ export default class RolesRoomSettingsTab extends React.Component {
const eventsLevelPrefix = "event_levels_";
- value = parseInt(value);
+ const value = parseInt(inputValue);
if (powerLevelKey.startsWith(eventsLevelPrefix)) {
// deep copy "events" object, Object.assign itself won't deep copy
@@ -182,7 +186,7 @@ export default class RolesRoomSettingsTab extends React.Component {
});
};
- _onUserPowerLevelChanged = (value, powerLevelKey) => {
+ private onUserPowerLevelChanged = (value: string, powerLevelKey: string) => {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
const plEvent = room.currentState.getStateEvents('m.room.power_levels', '');
@@ -266,7 +270,7 @@ export default class RolesRoomSettingsTab extends React.Component {
currentUserLevel = defaultUserLevel;
}
- this._populateDefaultPlEvents(
+ this.populateDefaultPlEvents(
eventsLevels,
parseIntWithDefault(plContent.state_default, powerLevelDescriptors.state_default.defaultValue),
parseIntWithDefault(plContent.events_default, powerLevelDescriptors.events_default.defaultValue),
@@ -288,7 +292,7 @@ export default class RolesRoomSettingsTab extends React.Component {
label={user}
key={user}
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
- onChange={this._onUserPowerLevelChanged}
+ onChange={this.onUserPowerLevelChanged}
/>,
);
} else if (userLevels[user] < defaultUserLevel) { // muted
@@ -299,7 +303,7 @@ export default class RolesRoomSettingsTab extends React.Component {
label={user}
key={user}
powerLevelKey={user} // Will be sent as the second parameter to `onChange`
- onChange={this._onUserPowerLevelChanged}
+ onChange={this.onUserPowerLevelChanged}
/>,
);
}
@@ -345,8 +349,9 @@ export default class RolesRoomSettingsTab extends React.Component {
if (sender) bannedBy = sender.name;
return (
+ member={member} reason={banEvent.reason}
+ by={bannedBy}
+ />
);
})}
@@ -373,7 +378,7 @@ export default class RolesRoomSettingsTab extends React.Component {
usersDefault={defaultUserLevel}
disabled={!canChangeLevels || currentUserLevel < value}
powerLevelKey={key} // Will be sent as the second parameter to `onChange`
- onChange={this._onPowerLevelsChanged}
+ onChange={this.onPowerLevelsChanged}
/>
- {
- _t( "If you've submitted a bug via GitHub, debug logs can help " +
- "us track down the problem. Debug logs contain application " +
- "usage data including your username, the IDs or aliases of " +
- "the rooms or groups you have visited and the usernames of " +
- "other users. They do not contain messages.",
- )
- }
+ {_t(
+ "If you've submitted a bug via GitHub, debug logs can help " +
+ "us track down the problem. Debug logs contain application " +
+ "usage data including your username, the IDs or aliases of " +
+ "the rooms or groups you have visited and the usernames of " +
+ "other users. They do not contain messages.",
+ )}