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

This commit is contained in:
Michael Telatynski 2021-06-24 09:33:39 +01:00
commit 0e2f617d94
217 changed files with 4927 additions and 2789 deletions

3
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1,3 @@
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request -->
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off -->

View file

@ -1,4 +1,4 @@
name: Develop jobs name: Develop
on: on:
push: push:
branches: [develop] branches: [develop]
@ -11,10 +11,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: End-to-End tests - name: Prepare End-to-End tests
run: ./scripts/ci/end-to-end-tests.sh run: ./scripts/ci/prepare-end-to-end-tests.sh
- name: Run End-to-End tests
run: ./scripts/ci/run-end-to-end-tests.sh
- name: Archive logs - name: Archive logs
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
if: ${{ always() }}
with: with:
path: | path: |
test/end-to-end-tests/logs/**/* test/end-to-end-tests/logs/**/*

View file

@ -1,3 +1,133 @@
Changes in [3.24.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0) (2021-06-21)
=====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.24.0-rc.1...v3.24.0)
* Upgrade to JS SDK 12.0.0
* [Release] Keep composer reply when scrolling away from a highlighted event
[\#6211](https://github.com/matrix-org/matrix-react-sdk/pull/6211)
* [Release] Remove stray bullet point in reply preview
[\#6210](https://github.com/matrix-org/matrix-react-sdk/pull/6210)
* [Release] Stop requesting null next replies from the server
[\#6209](https://github.com/matrix-org/matrix-react-sdk/pull/6209)
Changes in [3.24.0-rc.1](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.24.0-rc.1) (2021-06-15)
===============================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0...v3.24.0-rc.1)
* Upgrade to JS SDK 12.0.0-rc.1
* Translations update from Weblate
[\#6192](https://github.com/matrix-org/matrix-react-sdk/pull/6192)
* Disable comment-on-alert for PR coming from a fork
[\#6189](https://github.com/matrix-org/matrix-react-sdk/pull/6189)
* Add JS benchmark tracking in CI
[\#6177](https://github.com/matrix-org/matrix-react-sdk/pull/6177)
* Upgrade matrix-react-test-utils for React 17 peer deps
[\#6187](https://github.com/matrix-org/matrix-react-sdk/pull/6187)
* Fix display name overlaps on the IRC layout
[\#6186](https://github.com/matrix-org/matrix-react-sdk/pull/6186)
* Small fixes to the spaces experience
[\#6184](https://github.com/matrix-org/matrix-react-sdk/pull/6184)
* Add footer and privacy note to the start dm dialog
[\#6111](https://github.com/matrix-org/matrix-react-sdk/pull/6111)
* Format mxids when disambiguation needed
[\#5880](https://github.com/matrix-org/matrix-react-sdk/pull/5880)
* Move various createRoom types to the js-sdk
[\#6183](https://github.com/matrix-org/matrix-react-sdk/pull/6183)
* Fix HTML tag for Event Tile when not rendered in a list
[\#6175](https://github.com/matrix-org/matrix-react-sdk/pull/6175)
* Remove legacy polyfills and unused dependencies
[\#6176](https://github.com/matrix-org/matrix-react-sdk/pull/6176)
* Fix buggy hovering/selecting of event tiles
[\#6173](https://github.com/matrix-org/matrix-react-sdk/pull/6173)
* Add room intro warning when e2ee is not enabled
[\#5929](https://github.com/matrix-org/matrix-react-sdk/pull/5929)
* Migrate end to end tests to GitHub actions
[\#6156](https://github.com/matrix-org/matrix-react-sdk/pull/6156)
* Fix expanding last collapsed sticky session when zoomed in
[\#6171](https://github.com/matrix-org/matrix-react-sdk/pull/6171)
* ⚛️ Upgrade to React@17
[\#6165](https://github.com/matrix-org/matrix-react-sdk/pull/6165)
* Revert refreshStickyHeaders optimisations
[\#6168](https://github.com/matrix-org/matrix-react-sdk/pull/6168)
* Add logging for which rooms calls are in
[\#6170](https://github.com/matrix-org/matrix-react-sdk/pull/6170)
* Restore read receipt animation from event to event
[\#6169](https://github.com/matrix-org/matrix-react-sdk/pull/6169)
* Restore copy button icon when sharing permalink
[\#6166](https://github.com/matrix-org/matrix-react-sdk/pull/6166)
* Restore Page Up/Down key bindings when focusing the composer
[\#6167](https://github.com/matrix-org/matrix-react-sdk/pull/6167)
* Timeline rendering optimizations
[\#6143](https://github.com/matrix-org/matrix-react-sdk/pull/6143)
* Bump css-what from 5.0.0 to 5.0.1
[\#6164](https://github.com/matrix-org/matrix-react-sdk/pull/6164)
* Bump ws from 6.2.1 to 6.2.2 in /test/end-to-end-tests
[\#6145](https://github.com/matrix-org/matrix-react-sdk/pull/6145)
* Bump trim-newlines from 3.0.0 to 3.0.1
[\#6163](https://github.com/matrix-org/matrix-react-sdk/pull/6163)
* Fix upgrade to element home button in top left menu
[\#6162](https://github.com/matrix-org/matrix-react-sdk/pull/6162)
* Fix unpinning of pinned messages and panel empty state
[\#6140](https://github.com/matrix-org/matrix-react-sdk/pull/6140)
* Better handling for widgets that fail to load
[\#6161](https://github.com/matrix-org/matrix-react-sdk/pull/6161)
* Improved forwarding UI
[\#5999](https://github.com/matrix-org/matrix-react-sdk/pull/5999)
* Fixes for sharing room links
[\#6118](https://github.com/matrix-org/matrix-react-sdk/pull/6118)
* Fix setting watchers
[\#6160](https://github.com/matrix-org/matrix-react-sdk/pull/6160)
* Fix Stickerpicker context menu
[\#6152](https://github.com/matrix-org/matrix-react-sdk/pull/6152)
* Add warning to private space creation flow
[\#6155](https://github.com/matrix-org/matrix-react-sdk/pull/6155)
* Add prop to alwaysShowTimestamps on TimelinePanel
[\#6159](https://github.com/matrix-org/matrix-react-sdk/pull/6159)
* Fix notif panel timestamp padding
[\#6157](https://github.com/matrix-org/matrix-react-sdk/pull/6157)
* Fixes and refactoring for the ImageView
[\#6149](https://github.com/matrix-org/matrix-react-sdk/pull/6149)
* Fix timestamps
[\#6148](https://github.com/matrix-org/matrix-react-sdk/pull/6148)
* Make it easier to pan images in the lightbox
[\#6147](https://github.com/matrix-org/matrix-react-sdk/pull/6147)
* Fix scroll token for EventTile and EventListSummary node type
[\#6154](https://github.com/matrix-org/matrix-react-sdk/pull/6154)
* Convert bunch of things to Typescript
[\#6153](https://github.com/matrix-org/matrix-react-sdk/pull/6153)
* Lint the typescript tests
[\#6142](https://github.com/matrix-org/matrix-react-sdk/pull/6142)
* Fix jumping to bottom without a highlighted event
[\#6146](https://github.com/matrix-org/matrix-react-sdk/pull/6146)
* Repair event status position in timeline
[\#6141](https://github.com/matrix-org/matrix-react-sdk/pull/6141)
* Adapt for js-sdk MatrixClient conversion to TS
[\#6132](https://github.com/matrix-org/matrix-react-sdk/pull/6132)
* Improve pinned messages in Labs
[\#6096](https://github.com/matrix-org/matrix-react-sdk/pull/6096)
* Map phone number lookup results to their native rooms
[\#6136](https://github.com/matrix-org/matrix-react-sdk/pull/6136)
* Fix mx_Event containment rules and empty read avatar row
[\#6138](https://github.com/matrix-org/matrix-react-sdk/pull/6138)
* Improve switch room rendering
[\#6079](https://github.com/matrix-org/matrix-react-sdk/pull/6079)
* Add CSS containment rules for shorter reflow operations
[\#6127](https://github.com/matrix-org/matrix-react-sdk/pull/6127)
* ignore hash/fragment when de-duplicating links for url previews
[\#6135](https://github.com/matrix-org/matrix-react-sdk/pull/6135)
* Clicking jump to bottom resets room hash
[\#5823](https://github.com/matrix-org/matrix-react-sdk/pull/5823)
* Use passive option for scroll handlers
[\#6113](https://github.com/matrix-org/matrix-react-sdk/pull/6113)
* Optimise memberSort performance for large list
[\#6130](https://github.com/matrix-org/matrix-react-sdk/pull/6130)
* Tweak event border radius to match action bar
[\#6133](https://github.com/matrix-org/matrix-react-sdk/pull/6133)
* Log when we ignore a second call in a room
[\#6131](https://github.com/matrix-org/matrix-react-sdk/pull/6131)
* Performance monitoring measurements
[\#6041](https://github.com/matrix-org/matrix-react-sdk/pull/6041)
Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07) Changes in [3.23.0](https://github.com/matrix-org/matrix-react-sdk/releases/tag/v3.23.0) (2021-06-07)
===================================================================================================== =====================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0) [Full Changelog](https://github.com/matrix-org/matrix-react-sdk/compare/v3.23.0-rc.1...v3.23.0)

View file

@ -1,6 +1,6 @@
{ {
"name": "matrix-react-sdk", "name": "matrix-react-sdk",
"version": "3.23.0", "version": "3.24.0",
"description": "SDK for matrix.org using React", "description": "SDK for matrix.org using React",
"author": "matrix.org", "author": "matrix.org",
"repository": { "repository": {
@ -78,8 +78,8 @@
"katex": "^0.12.0", "katex": "^0.12.0",
"linkifyjs": "^2.1.9", "linkifyjs": "^2.1.9",
"lodash": "^4.17.20", "lodash": "^4.17.20",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-js-sdk": "12.0.0",
"matrix-widget-api": "^0.1.0-beta.14", "matrix-widget-api": "^0.1.0-beta.15",
"minimist": "^1.2.5", "minimist": "^1.2.5",
"opus-recorder": "^8.0.3", "opus-recorder": "^8.0.3",
"pako": "^2.0.3", "pako": "^2.0.3",
@ -89,7 +89,7 @@
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
"re-resizable": "^6.9.0", "re-resizable": "^6.9.0",
"react": "^17.0.2", "react": "^17.0.2",
"react-beautiful-dnd": "^4.0.1", "react-beautiful-dnd": "^13.1.0",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-focus-lock": "^2.5.0", "react-focus-lock": "^2.5.0",
"react-transition-group": "^4.4.1", "react-transition-group": "^4.4.1",
@ -123,6 +123,7 @@
"@sinonjs/fake-timers": "^7.0.2", "@sinonjs/fake-timers": "^7.0.2",
"@types/classnames": "^2.2.11", "@types/classnames": "^2.2.11",
"@types/counterpart": "^0.18.1", "@types/counterpart": "^0.18.1",
"@types/diff-match-patch": "^1.0.32",
"@types/flux": "^3.1.9", "@types/flux": "^3.1.9",
"@types/jest": "^26.0.20", "@types/jest": "^26.0.20",
"@types/linkifyjs": "^2.1.3", "@types/linkifyjs": "^2.1.3",
@ -132,19 +133,20 @@
"@types/pako": "^1.0.1", "@types/pako": "^1.0.1",
"@types/parse5": "^6.0.0", "@types/parse5": "^6.0.0",
"@types/qrcode": "^1.3.5", "@types/qrcode": "^1.3.5",
"@types/react": "^16.9", "@types/react": "^17.0.2",
"@types/react-dom": "^16.9.10", "@types/react-beautiful-dnd": "^13.0.0",
"@types/react-dom": "^17.0.2",
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1", "@types/sanitize-html": "^2.3.1",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^4.14.0", "@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0", "@typescript-eslint/parser": "^4.14.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3", "babel-jest": "^26.6.3",
"chokidar": "^3.5.1", "chokidar": "^3.5.1",
"concurrently": "^5.3.0", "concurrently": "^5.3.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"eslint": "7.18.0", "eslint": "7.18.0",
"eslint-config-matrix-org": "^0.2.0", "eslint-config-matrix-org": "^0.2.0",
"eslint-plugin-babel": "^5.3.1", "eslint-plugin-babel": "^5.3.1",
@ -167,9 +169,6 @@
"typescript": "^4.1.3", "typescript": "^4.1.3",
"walk": "^2.3.14" "walk": "^2.3.14"
}, },
"resolutions": {
"**/@types/react": "^16.14"
},
"jest": { "jest": {
"testEnvironment": "./__test-utils__/environment.js", "testEnvironment": "./__test-utils__/environment.js",
"testMatch": [ "testMatch": [

View file

@ -123,7 +123,6 @@
@import "./views/elements/_EventListSummary.scss"; @import "./views/elements/_EventListSummary.scss";
@import "./views/elements/_FacePile.scss"; @import "./views/elements/_FacePile.scss";
@import "./views/elements/_Field.scss"; @import "./views/elements/_Field.scss";
@import "./views/elements/_FormButton.scss";
@import "./views/elements/_ImageView.scss"; @import "./views/elements/_ImageView.scss";
@import "./views/elements/_InfoTooltip.scss"; @import "./views/elements/_InfoTooltip.scss";
@import "./views/elements/_InlineSpinner.scss"; @import "./views/elements/_InlineSpinner.scss";

View file

@ -31,7 +31,6 @@ $activeBorderColor: $secondary-fg-color;
// Create another flexbox so the Panel fills the container // Create another flexbox so the Panel fills the container
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow-y: auto;
.mx_SpacePanel_spaceTreeWrapper { .mx_SpacePanel_spaceTreeWrapper {
flex: 1; flex: 1;
@ -69,6 +68,12 @@ $activeBorderColor: $secondary-fg-color;
cursor: pointer; cursor: pointer;
} }
.mx_SpaceItem_dragging {
.mx_SpaceButton_toggleCollapse {
visibility: hidden;
}
}
.mx_SpaceTreeLevel { .mx_SpaceTreeLevel {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View file

@ -71,7 +71,7 @@ limitations under the License.
&::before { &::before {
background-color: #ffffff; background-color: #ffffff;
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-size: 90%; mask-size: 80%;
} }
&::after { &::after {
@ -134,8 +134,9 @@ limitations under the License.
.mx_Toast_buttons { .mx_Toast_buttons {
float: right; float: right;
display: flex; display: flex;
gap: 5px;
.mx_FormButton { .mx_AccessibleButton {
min-width: 96px; min-width: 96px;
box-sizing: border-box; box-sizing: border-box;
} }

View file

@ -19,49 +19,68 @@ limitations under the License.
padding: 24px; padding: 24px;
background-color: $settings-profile-placeholder-bg-color; background-color: $settings-profile-placeholder-bg-color;
border-radius: 8px; border-radius: 8px;
display: flex;
box-sizing: border-box; box-sizing: border-box;
> div { .mx_BetaCard_columns {
.mx_BetaCard_title { display: flex;
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
color: $primary-fg-color;
margin: 4px 0 14px;
.mx_BetaCard_betaPill { > div {
margin-left: 12px; .mx_BetaCard_title {
font-weight: $font-semi-bold;
font-size: $font-18px;
line-height: $font-22px;
color: $primary-fg-color;
margin: 4px 0 14px;
.mx_BetaCard_betaPill {
margin-left: 12px;
}
}
.mx_BetaCard_caption {
font-size: $font-15px;
line-height: $font-20px;
color: $secondary-fg-color;
margin-bottom: 20px;
}
.mx_BetaCard_buttons .mx_AccessibleButton {
display: block;
margin: 12px 0;
padding: 7px 40px;
width: auto;
}
.mx_BetaCard_disclaimer {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
margin-top: 20px;
} }
} }
.mx_BetaCard_caption { > img {
font-size: $font-15px; margin: auto 0 auto 20px;
line-height: $font-20px; width: 300px;
color: $secondary-fg-color; object-fit: contain;
margin-bottom: 20px; height: 100%;
}
.mx_AccessibleButton {
display: block;
margin: 12px 0;
padding: 7px 40px;
width: auto;
}
.mx_BetaCard_disclaimer {
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
margin-top: 20px;
} }
} }
> img { .mx_BetaCard_relatedSettings {
margin: auto 0 auto 20px; .mx_SettingsFlag {
width: 300px; margin: 16px 0 0;
object-fit: contain; font-size: $font-15px;
height: 100%; line-height: $font-24px;
color: $primary-fg-color;
.mx_SettingsFlag_microcopy {
margin-top: 4px;
font-size: $font-12px;
line-height: $font-15px;
color: $secondary-fg-color;
}
}
} }
} }

View file

@ -38,6 +38,15 @@ limitations under the License.
mask-image: url('$(res)/img/element-icons/view-community.svg'); mask-image: url('$(res)/img/element-icons/view-community.svg');
} }
.mx_TagTileContextMenu_moveUp::before {
transform: rotate(180deg);
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
.mx_TagTileContextMenu_moveDown::before {
mask-image: url('$(res)/img/feather-customised/chevron-down.svg');
}
.mx_TagTileContextMenu_hideCommunity::before { .mx_TagTileContextMenu_hideCommunity::before {
mask-image: url('$(res)/img/element-icons/hide.svg'); mask-image: url('$(res)/img/element-icons/hide.svg');
} }

View file

@ -15,7 +15,7 @@ limitations under the License.
*/ */
// Not actually a component but things shared by settings components // Not actually a component but things shared by settings components
.mx_UserSettingsDialog, .mx_RoomSettingsDialog { .mx_UserSettingsDialog, .mx_RoomSettingsDialog, .mx_SpaceSettingsDialog {
width: 90vw; width: 90vw;
max-width: 1000px; max-width: 1000px;
// set the height too since tabbed view scrolls itself. // set the height too since tabbed view scrolls itself.

View file

@ -15,7 +15,6 @@ limitations under the License.
*/ */
.mx_SpaceSettingsDialog { .mx_SpaceSettingsDialog {
width: 480px;
color: $primary-fg-color; color: $primary-fg-color;
.mx_SpaceSettings_errorText { .mx_SpaceSettings_errorText {
@ -32,8 +31,44 @@ limitations under the License.
margin-left: 16px; margin-left: 16px;
} }
.mx_AccessibleButton_kind_danger { .mx_SettingsTab_section {
margin-top: 28px; .mx_SettingsTab_section_caption {
margin-top: 12px;
margin-bottom: 20px;
}
& + .mx_SettingsTab_subheading {
border-top: 1px solid $message-body-panel-bg-color;
margin-top: 0;
padding-top: 24px;
}
.mx_RadioButton {
margin-top: 8px;
margin-bottom: 4px;
.mx_RadioButton_content {
font-weight: $font-semi-bold;
line-height: $font-18px;
color: $primary-fg-color;
}
& + span {
font-size: $font-15px;
line-height: $font-18px;
color: $secondary-fg-color;
margin-left: 26px;
}
}
.mx_SettingsTab_showAdvanced {
margin: 16px 0;
padding: 0;
}
.mx_SettingsFlag {
margin-top: 24px;
}
} }
.mx_SpaceSettingsDialog_buttons { .mx_SpaceSettingsDialog_buttons {
@ -52,4 +87,14 @@ limitations under the License.
.mx_AccessibleButton_hasKind { .mx_AccessibleButton_hasKind {
padding: 8px 22px; padding: 8px 22px;
} }
.mx_TabbedView_tabLabel {
.mx_SpaceSettingsDialog_generalIcon::before {
mask-image: url('$(res)/img/element-icons/settings.svg');
}
.mx_SpaceSettingsDialog_visibilityIcon::before {
mask-image: url('$(res)/img/element-icons/eye.svg');
}
}
} }

View file

@ -1,42 +0,0 @@
/*
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.
*/
.mx_FormButton {
line-height: $font-16px;
padding: 5px 15px;
font-size: $font-12px;
height: min-content;
&:not(:last-child) {
margin-right: 8px;
}
&.mx_AccessibleButton_kind_primary {
color: $accent-color;
background-color: $accent-bg-color;
}
&.mx_AccessibleButton_kind_danger {
color: $notice-primary-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

@ -21,7 +21,7 @@ limitations under the License.
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
&.mx_cryptoEvent_icon::after { &.mx_cryptoEvent_icon::after {

View file

@ -259,16 +259,6 @@ limitations under the License.
.mx_AccessibleButton.mx_AccessibleButton_hasKind { .mx_AccessibleButton.mx_AccessibleButton_hasKind {
padding: 8px 18px; padding: 8px 18px;
&.mx_AccessibleButton_kind_primary {
color: $accent-color;
background-color: $accent-bg-color;
}
&.mx_AccessibleButton_kind_danger {
color: $notice-primary-color;
background-color: $notice-primary-bg-color;
}
} }
.mx_VerificationShowSas .mx_AccessibleButton, .mx_VerificationShowSas .mx_AccessibleButton,

View file

@ -58,7 +58,7 @@ limitations under the License.
} }
.mx_VerificationPanel_reciprocate_section { .mx_VerificationPanel_reciprocate_section {
.mx_FormButton { .mx_AccessibleButton {
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
padding: 10px; padding: 10px;

View file

@ -45,7 +45,7 @@ limitations under the License.
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
// transparent-looking border surrounding the shield for when overlain over avatars // transparent-looking border surrounding the shield for when overlain over avatars
@ -59,7 +59,7 @@ limitations under the License.
} }
// shrink the infill of the badge // shrink the infill of the badge
&::before { &::before {
mask-size: 65%; mask-size: 60%;
} }
} }

View file

@ -345,7 +345,7 @@ $hover-select-border: 4px;
mask-image: url('$(res)/img/e2e/normal.svg'); mask-image: url('$(res)/img/e2e/normal.svg');
mask-repeat: no-repeat; mask-repeat: no-repeat;
mask-position: center; mask-position: center;
mask-size: 90%; mask-size: 80%;
} }
} }

View file

@ -16,7 +16,7 @@ limitations under the License.
.mx_SpaceBasicSettings { .mx_SpaceBasicSettings {
.mx_Field { .mx_Field {
margin: 32px 0; margin: 24px 0;
} }
.mx_SpaceBasicSettings_avatarContainer { .mx_SpaceBasicSettings_avatarContainer {
@ -73,7 +73,7 @@ limitations under the License.
} }
} }
.mx_FormButton { .mx_AccessibleButton {
padding: 8px 22px; padding: 8px 22px;
margin-left: auto; margin-left: auto;
display: block; display: block;

View file

@ -98,5 +98,29 @@ limitations under the License.
line-height: $font-24px; line-height: $font-24px;
} }
} }
.mx_IncomingCallBox_iconButton {
position: absolute;
right: 8px;
&::before {
content: '';
height: 20px;
width: 20px;
background-color: $icon-button-color;
mask-repeat: no-repeat;
mask-size: contain;
mask-position: center;
}
}
.mx_IncomingCallBox_silence::before {
mask-image: url('$(res)/img/voip/silence.svg');
}
.mx_IncomingCallBox_unSilence::before {
mask-image: url('$(res)/img/voip/un-silence.svg');
}
} }
} }

View file

@ -23,7 +23,7 @@ limitations under the License.
.mx_DialPad_button { .mx_DialPad_button {
width: 40px; width: 40px;
height: 40px; height: 40px;
background-color: $theme-button-bg-color; background-color: $dialpad-button-bg-color;
border-radius: 40px; border-radius: 40px;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;

View file

@ -27,9 +27,22 @@ limitations under the License.
} }
.mx_DialPadContextMenu_dialled { .mx_DialPadContextMenu_dialled {
height: 1em; height: 1.5em;
font-size: 18px; font-size: 18px;
font-weight: 600; font-weight: 600;
max-width: 150px;
border: none;
margin: 0px;
}
.mx_DialPadContextMenu_dialled input {
font-size: 18px;
font-weight: 600;
overflow: hidden;
max-width: 150px;
text-align: left;
direction: rtl;
padding: 8px 0px;
background-color: rgb(0, 0, 0, 0);
} }
.mx_DialPadContextMenu_dialPad { .mx_DialPadContextMenu_dialPad {

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.3094 5.96587C15.3206 7.15704 15.3417 8.85457 14.3412 10.0548C13.0889 11.5571 10.9822 13.3332 8.02104 13.3332C5.05992 13.3332 2.9532 11.5571 1.70087 10.0548C0.700398 8.85457 0.721506 7.15704 1.7327 5.96587C3.01174 4.45918 5.1391 2.6665 8.02104 2.6665C10.903 2.6665 13.0303 4.45918 14.3094 5.96587ZM11.5556 7.99984C11.5556 9.96352 9.96369 11.5554 8.00001 11.5554C6.03633 11.5554 4.44446 9.96352 4.44446 7.99984C4.44446 6.03616 6.03633 4.44428 8.00001 4.44428C9.96369 4.44428 11.5556 6.03616 11.5556 7.99984ZM8.00001 9.77761C8.98185 9.77761 9.77779 8.98168 9.77779 7.99984C9.77779 7.018 8.98185 6.22206 8.00001 6.22206C7.01817 6.22206 6.22224 7.018 6.22224 7.99984C6.22224 8.98168 7.01817 9.77761 8.00001 9.77761Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 887 B

3
res/img/voip/silence.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.56986 1.82566L4 4.80054L1.5 4.80054C0.671573 4.80054 0 5.47212 0 6.30054V9.70054C0 10.529 0.671573 11.2005 1.5 11.2005L4 11.2005L7.56986 14.1754C8.05836 14.5825 8.8 14.2351 8.8 13.5993L8.8 9.70054V6.30054L8.8 2.40182C8.8 1.76595 8.05836 1.41858 7.56986 1.82566ZM14.1546 2.76877C13.9162 2.46224 13.4745 2.40702 13.1679 2.64543L13.0443 3.6318L13.0446 3.63212L13.0448 3.63238L13.0536 3.64417C13.0623 3.65582 13.0764 3.67498 13.0951 3.7013C13.1325 3.75399 13.1883 3.83518 13.2564 3.94222C13.3929 4.15668 13.5774 4.47271 13.7624 4.86922C14.1345 5.66647 14.4965 6.763 14.4965 8.00044C14.4965 9.23789 14.1345 10.3344 13.7624 11.1317C13.5774 11.5282 13.3929 11.8442 13.2564 12.0587C13.1883 12.1657 13.1325 12.2469 13.0951 12.2996C13.0764 12.3259 13.0623 12.3451 13.0536 12.3567L13.0448 12.3685L13.0446 12.3688L13.0443 12.3691L13.0441 12.3694L13.0438 12.3698L13.0436 12.37C12.8063 12.6765 12.8618 13.1174 13.1679 13.3555C13.4745 13.5939 13.9162 13.5386 14.1546 13.2321L13.5996 12.8004C14.1546 13.2321 14.1548 13.2319 14.1549 13.2317L14.1552 13.2313L14.156 13.2303L14.158 13.2278L14.1636 13.2204L14.1815 13.1966C14.1963 13.1768 14.2166 13.1491 14.2416 13.1138C14.2917 13.0433 14.3609 12.9423 14.4428 12.8136C14.6063 12.5567 14.8218 12.187 15.0368 11.7264C15.4647 10.8093 15.9027 9.50586 15.9027 8.00044C15.9027 6.49503 15.4647 5.19156 15.0368 4.27453C14.8218 3.8139 14.6063 3.44421 14.4428 3.18724C14.3609 3.05857 14.2917 2.95762 14.2416 2.88709C14.2166 2.8518 14.1963 2.82408 14.1815 2.80426L14.1636 2.78048L14.158 2.7731L14.156 2.77055L14.1552 2.76956L14.1549 2.76914C14.1548 2.76895 14.1546 2.76877 13.5996 3.20045L14.1546 2.76877ZM11.7552 5.16879C11.5168 4.86227 11.075 4.80705 10.7685 5.04546C10.4628 5.28321 10.4071 5.72319 10.6432 6.02961L10.6452 6.03231C10.6481 6.03609 10.6535 6.04353 10.6613 6.05445C10.6768 6.07633 10.7014 6.11199 10.732 6.1601C10.7935 6.2567 10.878 6.4013 10.963 6.58353C11.1351 6.95221 11.2971 7.44874 11.2971 8.00047C11.2971 8.5522 11.1351 9.04873 10.963 9.41741C10.878 9.59964 10.7935 9.74424 10.732 9.84084C10.7014 9.88895 10.6768 9.92461 10.6613 9.94648C10.6535 9.95741 10.6481 9.96484 10.6452 9.96863L10.6432 9.97132C10.4071 10.2777 10.4628 10.7177 10.7685 10.9555C11.075 11.1939 11.5168 11.1387 11.7552 10.8321L11.2002 10.4005C11.7552 10.8321 11.7553 10.832 11.7555 10.8318L11.7558 10.8314L11.7564 10.8305L11.758 10.8286L11.7619 10.8234L11.7731 10.8085C11.782 10.7966 11.7937 10.7806 11.8078 10.7607C11.8361 10.721 11.874 10.6656 11.9184 10.5958C12.0069 10.4567 12.1224 10.2584 12.2374 10.0121C12.4653 9.52364 12.7033 8.82017 12.7033 8.00047C12.7033 7.18077 12.4653 6.4773 12.2374 5.98884C12.1224 5.74249 12.0069 5.54424 11.9184 5.40512C11.874 5.33538 11.8361 5.27996 11.8078 5.24023C11.7937 5.22035 11.782 5.20435 11.7731 5.1924L11.7619 5.17752L11.758 5.17238L11.7564 5.17039L11.7558 5.16954L11.7555 5.16916C11.7553 5.16897 11.7552 5.16879 11.2002 5.60047L11.7552 5.16879Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -0,0 +1,4 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.18262 0.960693L14.3815 14.1596" stroke="#8D99A5" stroke-width="1.61751" stroke-miterlimit="10" stroke-linecap="round"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.57061 4.20635L5.19539 4.51904H3.58059L10.2419 11.1804V8.87764L5.57061 4.20635ZM10.2419 6.59013V2.15546C10.2419 1.42405 9.38884 1.0245 8.82695 1.49274L6.81834 3.16658L10.2419 6.59013ZM5.19526 11.2479H2.7146C1.76172 11.2479 0.989258 10.4754 0.989258 9.52254V6.24438C0.989258 5.69051 1.25024 5.1976 1.65595 4.88191L10.2419 13.4679V13.6117C10.2419 14.3431 9.38884 14.7426 8.82695 14.2744L5.19526 11.248V11.2479Z" fill="#8D99A5"/>
</svg>

After

Width:  |  Height:  |  Size: 713 B

View file

@ -118,6 +118,9 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $bg-color; $roomlist-filter-active-bg-color: $bg-color;

View file

@ -114,6 +114,8 @@ $voipcall-plinth-color: #394049;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #6F7882;
;
$roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;

View file

@ -181,6 +181,8 @@ $voipcall-plinth-color: #F4F6FA;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: $roomlist-button-bg-color; $roomlist-filter-active-bg-color: $roomlist-button-bg-color;

View file

@ -173,6 +173,8 @@ $voipcall-plinth-color: #F4F6FA;
// ******************** // ********************
$theme-button-bg-color: #e3e8f0; $theme-button-bg-color: #e3e8f0;
$dialpad-button-bg-color: #e3e8f0;
$roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons
$roomlist-filter-active-bg-color: #ffffff; $roomlist-filter-active-bg-color: #ffffff;

View file

@ -3,6 +3,6 @@
# docker push vectorim/element-web-ci-e2etests-env:latest # docker push vectorim/element-web-ci-e2etests-env:latest
FROM node:14-buster FROM node:14-buster
RUN apt-get update RUN apt-get update
RUN apt-get -y install build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime RUN apt-get -y install jq build-essential python3-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev uuid-runtime
# dependencies for chrome (installed by puppeteer) # dependencies for chrome (installed by puppeteer)
RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget RUN apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget

View file

@ -1,8 +1,4 @@
#!/bin/bash #!/bin/bash
#
# script which is run by the CI build (after `yarn test`).
#
# clones element-web develop and runs the tests against our version of react-sdk.
set -ev set -ev
@ -19,7 +15,7 @@ cd element-web
element_web_dir=`pwd` element_web_dir=`pwd`
CI_PACKAGE=true yarn build CI_PACKAGE=true yarn build
cd .. cd ..
# run end to end tests # prepare end to end tests
pushd test/end-to-end-tests pushd test/end-to-end-tests
ln -s $element_web_dir element/element-web ln -s $element_web_dir element/element-web
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh # PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
@ -28,9 +24,4 @@ echo "--- Install synapse & other dependencies"
./install.sh ./install.sh
# install static webserver to server symlinked local copy of element # install static webserver to server symlinked local copy of element
./element/install-webserver.sh ./element/install-webserver.sh
rm -r logs || true
mkdir logs
echo "+++ Running end-to-end tests"
TESTS_STARTED=1
./run.sh --no-sandbox --log-directory logs/
popd popd

View file

@ -0,0 +1,19 @@
#!/bin/bash
set -ev
handle_error() {
EXIT_CODE=$?
exit $EXIT_CODE
}
trap 'handle_error' ERR
# run end to end tests
pushd test/end-to-end-tests
rm -r logs || true
mkdir logs
echo "--- Running end-to-end tests"
TESTS_STARTED=1
./run.sh --no-sandbox --log-directory logs/
popd

View file

@ -22,29 +22,51 @@ clone() {
} }
# Try the PR author's branch in case it exists on the deps as well. # Try the PR author's branch in case it exists on the deps as well.
# First we check if BUILDKITE_BRANCH is defined, # First we check if GITHUB_HEAD_REF is defined,
# if it isn't we can assume this is a Netlify build # Then we check if BUILDKITE_BRANCH is defined,
if [ -z ${BUILDKITE_BRANCH+x} ]; then # if they aren't we can assume this is a Netlify build
# Netlify doesn't give us info about the fork so we have to get it from GitHub API if [ -n "$GITHUB_HEAD_REF" ]; then
apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" head=$GITHUB_HEAD_REF
apiEndpoint+=$REVIEW_ID elif [ -n "$BUILDKITE_BRANCH" ]; then
head=$(curl $apiEndpoint | jq -r '.head.label')
else
head=$BUILDKITE_BRANCH head=$BUILDKITE_BRANCH
else
# Netlify doesn't give us info about the fork so we have to get it from GitHub API
apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/"
apiEndpoint+=$REVIEW_ID
head=$(curl $apiEndpoint | jq -r '.head.label')
fi fi
# If head is set, it will contain either: # If head is set, it will contain on Buildkite either:
# * "branch" when the author's branch and target branch are in the same repo # * "branch" when the author's branch and target branch are in the same repo
# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build # * "fork:branch" when the author's branch is in their fork or if this is a Netlify build
# We can split on `:` into an array to check. # We can split on `:` into an array to check.
# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR
# to determine whether the branch is from a fork or not
BRANCH_ARRAY=(${head//:/ }) BRANCH_ARRAY=(${head//:/ })
if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then
clone $deforg $defrepo $BUILDKITE_BRANCH
if [ -n "$GITHUB_HEAD_REF" ]; then
if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then
clone $deforg $defrepo $GITHUB_HEAD_REF
else
REPO_ARRAY=(${GITHUB_REPOSITORY//\// })
clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF
fi
else
clone $deforg $defrepo $BUILDKITE_BRANCH
fi
elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then
clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]}
fi fi
# Try the target branch of the push or PR. # Try the target branch of the push or PR.
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH if [ -n $GITHUB_BASE_REF ]; then
clone $deforg $defrepo $GITHUB_BASE_REF
elif [ -n $BUILDKITE_PULL_REQUEST_BASE_BRANCH ]; then
clone $deforg $defrepo $BUILDKITE_PULL_REQUEST_BASE_BRANCH
fi
# Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds) # Try HEAD which is the branch name in Netlify (not BRANCH which is pull/xxxx/head for PR builds)
clone $deforg $defrepo $HEAD clone $deforg $defrepo $HEAD
# Use the default branch as the last resort. # Use the default branch as the last resort.

50
src/@types/diff-dom.ts Normal file
View file

@ -0,0 +1,50 @@
/*
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.
*/
declare module "diff-dom" {
enum Action {
AddElement = "addElement",
AddTextElement = "addTextElement",
RemoveTextElement = "removeTextElement",
RemoveElement = "removeElement",
ReplaceElement = "replaceElement",
ModifyTextElement = "modifyTextElement",
AddAttribute = "addAttribute",
RemoveAttribute = "removeAttribute",
ModifyAttribute = "modifyAttribute",
}
export interface IDiff {
action: Action;
name: string;
text?: string;
route: number[];
value: string;
element: unknown;
oldValue: string;
newValue: string;
}
interface IOpts {
}
export class DiffDOM {
public constructor(opts?: IOpts);
public apply(tree: unknown, diffs: IDiff[]): unknown;
public undo(tree: unknown, diffs: IDiff[]): unknown;
public diff(a: HTMLElement | string, b: HTMLElement | string): IDiff[];
}
}

View file

@ -44,6 +44,7 @@ import { EventIndexPeg } from "../indexing/EventIndexPeg";
import {VoiceRecordingStore} from "../stores/VoiceRecordingStore"; import {VoiceRecordingStore} from "../stores/VoiceRecordingStore";
import PerformanceMonitor from "../performance"; import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore"; import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
declare global { declare global {
interface Window { interface Window {
@ -84,6 +85,7 @@ declare global {
mxPerformanceMonitor: PerformanceMonitor; mxPerformanceMonitor: PerformanceMonitor;
mxPerformanceEntryNames: any; mxPerformanceEntryNames: any;
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore;
} }
interface Document { interface Document {
@ -111,19 +113,6 @@ declare global {
usageDetails?: {[key: string]: number}; usageDetails?: {[key: string]: number};
} }
export interface ISettledFulfilled<T> {
status: "fulfilled";
value: T;
}
export interface ISettledRejected {
status: "rejected";
reason: any;
}
interface PromiseConstructor {
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
}
interface HTMLAudioElement { interface HTMLAudioElement {
type?: string; type?: string;
// sinkId & setSinkId are experimental and typescript doesn't know about them // sinkId & setSinkId are experimental and typescript doesn't know about them

View file

@ -14,18 +14,22 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {User} from "matrix-js-sdk/src/models/user"; import { User } from "matrix-js-sdk/src/models/user";
import {Room} from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { ResizeMethod } from "matrix-js-sdk/src/@types/partials";
import DMRoomMap from './utils/DMRoomMap'; import DMRoomMap from './utils/DMRoomMap';
import {mediaFromMxc} from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
import SettingsStore from "./settings/SettingsStore"; import SettingsStore from "./settings/SettingsStore";
export type ResizeMethod = "crop" | "scale";
// Not to be used for BaseAvatar urls as that has similar default avatar fallback already // Not to be used for BaseAvatar urls as that has similar default avatar fallback already
export function avatarUrlForMember(member: RoomMember, width: number, height: number, resizeMethod: ResizeMethod) { export function avatarUrlForMember(
member: RoomMember,
width: number,
height: number,
resizeMethod: ResizeMethod,
): string {
let url: string; let url: string;
if (member?.getMxcAvatarUrl()) { if (member?.getMxcAvatarUrl()) {
url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod); url = mediaFromMxc(member.getMxcAvatarUrl()).getThumbnailOfSourceHttp(width, height, resizeMethod);
@ -39,7 +43,12 @@ export function avatarUrlForMember(member: RoomMember, width: number, height: nu
return url; return url;
} }
export function avatarUrlForUser(user: User, width: number, height: number, resizeMethod?: ResizeMethod) { export function avatarUrlForUser(
user: Pick<User, "avatarUrl">,
width: number,
height: number,
resizeMethod?: ResizeMethod,
): string | null {
if (!user.avatarUrl) return null; if (!user.avatarUrl) return null;
return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod); return mediaFromMxc(user.avatarUrl).getThumbnailOfSourceHttp(width, height, resizeMethod);
} }

View file

@ -99,7 +99,7 @@ const CHECK_PROTOCOLS_ATTEMPTS = 3;
// (and store the ID of their native room) // (and store the ID of their native room)
export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room'; export const VIRTUAL_ROOM_EVENT_TYPE = 'im.vector.is_virtual_room';
enum AudioID { export enum AudioID {
Ring = 'ringAudio', Ring = 'ringAudio',
Ringback = 'ringbackAudio', Ringback = 'ringbackAudio',
CallEnd = 'callendAudio', CallEnd = 'callendAudio',

View file

@ -1,85 +0,0 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore from "./settings/SettingsStore";
import {SettingLevel} from "./settings/SettingLevel";
import {setMatrixCallAudioInput, setMatrixCallVideoInput} from "matrix-js-sdk/src/matrix";
export default {
hasAnyLabeledDevices: async function() {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.some(d => !!d.label);
},
getDevices: function() {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
return navigator.mediaDevices.enumerateDevices().then(function(devices) {
const audiooutput = [];
const audioinput = [];
const videoinput = [];
devices.forEach((device) => {
switch (device.kind) {
case 'audiooutput': audiooutput.push(device); break;
case 'audioinput': audioinput.push(device); break;
case 'videoinput': videoinput.push(device); break;
}
});
// console.log("Loaded WebRTC Devices", mediaDevices);
return {
audiooutput,
audioinput,
videoinput,
};
}, (error) => { console.log('Unable to refresh WebRTC Devices: ', error); });
},
loadDevices: function() {
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
setMatrixCallAudioInput(audioDeviceId);
setMatrixCallVideoInput(videoDeviceId);
},
setAudioOutput: function(deviceId) {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
},
setAudioInput: function(deviceId) {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallAudioInput(deviceId);
},
setVideoInput: function(deviceId) {
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallVideoInput(deviceId);
},
getAudioOutput: function() {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
},
getAudioInput: function() {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
},
getVideoInput: function() {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
},
};

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2018 New Vector Ltd Copyright 2018 - 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.
@ -14,34 +14,40 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixError } from "matrix-js-sdk/src/http-api";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
export class DecryptionFailure { export class DecryptionFailure {
constructor(failedEventId, errorCode) { public readonly ts: number;
this.failedEventId = failedEventId;
this.errorCode = errorCode; constructor(public readonly failedEventId: string, public readonly errorCode: string) {
this.ts = Date.now(); this.ts = Date.now();
} }
} }
type TrackingFn = (count: number, trackedErrCode: string) => void;
type ErrCodeMapFn = (errcode: string) => string;
export class DecryptionFailureTracker { export class DecryptionFailureTracker {
// Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list // Array of items of type DecryptionFailure. Every `CHECK_INTERVAL_MS`, this list
// is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did // is checked for failures that happened > `GRACE_PERIOD_MS` ago. Those that did
// are accumulated in `failureCounts`. // are accumulated in `failureCounts`.
failures = []; public failures: DecryptionFailure[] = [];
// A histogram of the number of failures that will be tracked at the next tracking // A histogram of the number of failures that will be tracked at the next tracking
// interval, split by failure error code. // interval, split by failure error code.
failureCounts = { public failureCounts: Record<string, number> = {
// [errorCode]: 42 // [errorCode]: 42
}; };
// Event IDs of failures that were tracked previously // Event IDs of failures that were tracked previously
trackedEventHashMap = { public trackedEventHashMap: Record<string, boolean> = {
// [eventId]: true // [eventId]: true
}; };
// Set to an interval ID when `start` is called // Set to an interval ID when `start` is called
checkInterval = null; public checkInterval: NodeJS.Timeout = null;
trackInterval = null; public trackInterval: NodeJS.Timeout = null;
// Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`. // Spread the load on `Analytics` by tracking at a low frequency, `TRACK_INTERVAL_MS`.
static TRACK_INTERVAL_MS = 60000; static TRACK_INTERVAL_MS = 60000;
@ -67,7 +73,7 @@ export class DecryptionFailureTracker {
* @param {function?} errorCodeMapFn The function used to map error codes to the * @param {function?} errorCodeMapFn The function used to map error codes to the
* trackedErrorCode. If not provided, the `.code` of errors will be used. * trackedErrorCode. If not provided, the `.code` of errors will be used.
*/ */
constructor(fn, errorCodeMapFn) { constructor(private readonly fn: TrackingFn, private readonly errorCodeMapFn?: ErrCodeMapFn) {
if (!fn || typeof fn !== 'function') { if (!fn || typeof fn !== 'function') {
throw new Error('DecryptionFailureTracker requires tracking function'); throw new Error('DecryptionFailureTracker requires tracking function');
} }
@ -75,9 +81,6 @@ export class DecryptionFailureTracker {
if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') { if (errorCodeMapFn && typeof errorCodeMapFn !== 'function') {
throw new Error('DecryptionFailureTracker second constructor argument should be a function'); throw new Error('DecryptionFailureTracker second constructor argument should be a function');
} }
this._trackDecryptionFailure = fn;
this._mapErrorCode = errorCodeMapFn;
} }
// loadTrackedEventHashMap() { // loadTrackedEventHashMap() {
@ -88,7 +91,7 @@ export class DecryptionFailureTracker {
// localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap)); // localStorage.setItem('mx-decryption-failure-event-id-hashes', JSON.stringify(this.trackedEventHashMap));
// } // }
eventDecrypted(e, err) { public eventDecrypted(e: MatrixEvent, err: MatrixError | Error): void {
if (err) { if (err) {
this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code)); this.addDecryptionFailure(new DecryptionFailure(e.getId(), err.code));
} else { } else {
@ -97,18 +100,18 @@ export class DecryptionFailureTracker {
} }
} }
addDecryptionFailure(failure) { public addDecryptionFailure(failure: DecryptionFailure): void {
this.failures.push(failure); this.failures.push(failure);
} }
removeDecryptionFailuresForEvent(e) { public removeDecryptionFailuresForEvent(e: MatrixEvent): void {
this.failures = this.failures.filter((f) => f.failedEventId !== e.getId()); this.failures = this.failures.filter((f) => f.failedEventId !== e.getId());
} }
/** /**
* Start checking for and tracking failures. * Start checking for and tracking failures.
*/ */
start() { public start(): void {
this.checkInterval = setInterval( this.checkInterval = setInterval(
() => this.checkFailures(Date.now()), () => this.checkFailures(Date.now()),
DecryptionFailureTracker.CHECK_INTERVAL_MS, DecryptionFailureTracker.CHECK_INTERVAL_MS,
@ -123,7 +126,7 @@ export class DecryptionFailureTracker {
/** /**
* Clear state and stop checking for and tracking failures. * Clear state and stop checking for and tracking failures.
*/ */
stop() { public stop(): void {
clearInterval(this.checkInterval); clearInterval(this.checkInterval);
clearInterval(this.trackInterval); clearInterval(this.trackInterval);
@ -132,11 +135,11 @@ export class DecryptionFailureTracker {
} }
/** /**
* Mark failures that occured before nowTs - GRACE_PERIOD_MS as failures that should be * Mark failures that occurred before nowTs - GRACE_PERIOD_MS as failures that should be
* tracked. Only mark one failure per event ID. * tracked. Only mark one failure per event ID.
* @param {number} nowTs the timestamp that represents the time now. * @param {number} nowTs the timestamp that represents the time now.
*/ */
checkFailures(nowTs) { public checkFailures(nowTs: number): void {
const failuresGivenGrace = []; const failuresGivenGrace = [];
const failuresNotReady = []; const failuresNotReady = [];
while (this.failures.length > 0) { while (this.failures.length > 0) {
@ -175,10 +178,10 @@ export class DecryptionFailureTracker {
const dedupedFailures = dedupedFailuresMap.values(); const dedupedFailures = dedupedFailuresMap.values();
this._aggregateFailures(dedupedFailures); this.aggregateFailures(dedupedFailures);
} }
_aggregateFailures(failures) { private aggregateFailures(failures: DecryptionFailure[]): void {
for (const failure of failures) { for (const failure of failures) {
const errorCode = failure.errorCode; const errorCode = failure.errorCode;
this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1; this.failureCounts[errorCode] = (this.failureCounts[errorCode] || 0) + 1;
@ -189,12 +192,12 @@ export class DecryptionFailureTracker {
* If there are failures that should be tracked, call the given trackDecryptionFailure * If there are failures that should be tracked, call the given trackDecryptionFailure
* function with the number of failures that should be tracked. * function with the number of failures that should be tracked.
*/ */
trackFailures() { public trackFailures(): void {
for (const errorCode of Object.keys(this.failureCounts)) { for (const errorCode of Object.keys(this.failureCounts)) {
if (this.failureCounts[errorCode] > 0) { if (this.failureCounts[errorCode] > 0) {
const trackedErrorCode = this._mapErrorCode ? this._mapErrorCode(errorCode) : errorCode; const trackedErrorCode = this.errorCodeMapFn ? this.errorCodeMapFn(errorCode) : errorCode;
this._trackDecryptionFailure(this.failureCounts[errorCode], trackedErrorCode); this.fn(this.failureCounts[errorCode], trackedErrorCode);
this.failureCounts[errorCode] = 0; this.failureCounts[errorCode] = 0;
} }
} }

View file

@ -17,11 +17,10 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from 'react'; import React, { ReactNode } from 'react';
import sanitizeHtml from 'sanitize-html'; import sanitizeHtml from 'sanitize-html';
import { IExtendedSanitizeOptions } from './@types/sanitize-html'; import cheerio from 'cheerio';
import * as linkify from 'linkifyjs'; import * as linkify from 'linkifyjs';
import linkifyMatrix from './linkify-matrix';
import _linkifyElement from 'linkifyjs/element'; import _linkifyElement from 'linkifyjs/element';
import _linkifyString from 'linkifyjs/string'; import _linkifyString from 'linkifyjs/string';
import classNames from 'classnames'; import classNames from 'classnames';
@ -29,13 +28,15 @@ import EMOJIBASE_REGEX from 'emojibase-regex';
import url from 'url'; import url from 'url';
import katex from 'katex'; import katex from 'katex';
import { AllHtmlEntities } from 'html-entities'; import { AllHtmlEntities } from 'html-entities';
import SettingsStore from './settings/SettingsStore'; import { IContent } from 'matrix-js-sdk/src/models/event';
import cheerio from 'cheerio';
import {tryTransformPermalinkToLocalHref} from "./utils/permalinks/Permalinks"; import { IExtendedSanitizeOptions } from './@types/sanitize-html';
import {SHORTCODE_TO_EMOJI, getEmojiFromUnicode} from "./emoji"; import linkifyMatrix from './linkify-matrix';
import SettingsStore from './settings/SettingsStore';
import { tryTransformPermalinkToLocalHref } from "./utils/permalinks/Permalinks";
import { SHORTCODE_TO_EMOJI, getEmojiFromUnicode } from "./emoji";
import ReplyThread from "./components/views/elements/ReplyThread"; import ReplyThread from "./components/views/elements/ReplyThread";
import {mediaFromMxc} from "./customisations/Media"; import { mediaFromMxc } from "./customisations/Media";
linkifyMatrix(linkify); linkifyMatrix(linkify);
@ -66,7 +67,7 @@ export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet'
* need emojification. * need emojification.
* unicodeToImage uses this function. * unicodeToImage uses this function.
*/ */
function mightContainEmoji(str: string) { function mightContainEmoji(str: string): boolean {
return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str); return SURROGATE_PAIR_PATTERN.test(str) || SYMBOL_PATTERN.test(str);
} }
@ -76,7 +77,7 @@ function mightContainEmoji(str: string) {
* @param {String} char The emoji character * @param {String} char The emoji character
* @return {String} The shortcode (such as :thumbup:) * @return {String} The shortcode (such as :thumbup:)
*/ */
export function unicodeToShortcode(char: string) { export function unicodeToShortcode(char: string): string {
const data = getEmojiFromUnicode(char); const data = getEmojiFromUnicode(char);
return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : ''); return (data && data.shortcodes ? `:${data.shortcodes[0]}:` : '');
} }
@ -87,7 +88,7 @@ export function unicodeToShortcode(char: string) {
* @param {String} shortcode The shortcode (such as :thumbup:) * @param {String} shortcode The shortcode (such as :thumbup:)
* @return {String} The emoji character; null if none exists * @return {String} The emoji character; null if none exists
*/ */
export function shortcodeToUnicode(shortcode: string) { export function shortcodeToUnicode(shortcode: string): string {
shortcode = shortcode.slice(1, shortcode.length - 1); shortcode = shortcode.slice(1, shortcode.length - 1);
const data = SHORTCODE_TO_EMOJI.get(shortcode); const data = SHORTCODE_TO_EMOJI.get(shortcode);
return data ? data.unicode : null; return data ? data.unicode : null;
@ -124,13 +125,13 @@ export function processHtmlForSending(html: string): string {
* Given an untrusted HTML string, return a React node with an sanitized version * Given an untrusted HTML string, return a React node with an sanitized version
* of that HTML. * of that HTML.
*/ */
export function sanitizedHtmlNode(insaneHtml: string) { export function sanitizedHtmlNode(insaneHtml: string): ReactNode {
const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams); const saneHtml = sanitizeHtml(insaneHtml, sanitizeHtmlParams);
return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />; return <div dangerouslySetInnerHTML={{ __html: saneHtml }} dir="auto" />;
} }
export function getHtmlText(insaneHtml: string) { export function getHtmlText(insaneHtml: string): string {
return sanitizeHtml(insaneHtml, { return sanitizeHtml(insaneHtml, {
allowedTags: [], allowedTags: [],
allowedAttributes: {}, allowedAttributes: {},
@ -148,7 +149,7 @@ export function getHtmlText(insaneHtml: string) {
* other places we need to sanitise URLs. * other places we need to sanitise URLs.
* @return true if permitted, otherwise false * @return true if permitted, otherwise false
*/ */
export function isUrlPermitted(inputUrl: string) { export function isUrlPermitted(inputUrl: string): boolean {
try { try {
const parsed = url.parse(inputUrl); const parsed = url.parse(inputUrl);
if (!parsed.protocol) return false; if (!parsed.protocol) return false;
@ -351,13 +352,6 @@ class HtmlHighlighter extends BaseHighlighter<string> {
} }
} }
interface IContent {
format?: string;
// eslint-disable-next-line camelcase
formatted_body?: string;
body: string;
}
interface IOpts { interface IOpts {
highlightLink?: string; highlightLink?: string;
disableBigEmoji?: boolean; disableBigEmoji?: boolean;
@ -367,6 +361,14 @@ interface IOpts {
ref?: React.Ref<any>; ref?: React.Ref<any>;
} }
export interface IOptsReturnNode extends IOpts {
returnString: false;
}
export interface IOptsReturnString extends IOpts {
returnString: true;
}
/* turn a matrix event body into html /* turn a matrix event body into html
* *
* content: 'content' of the MatrixEvent * content: 'content' of the MatrixEvent
@ -380,6 +382,8 @@ interface IOpts {
* opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer * opts.forComposerQuote: optional param to lessen the url rewriting done by sanitization, for quoting into composer
* opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString) * opts.ref: React ref to attach to any React components returned (not compatible with opts.returnString)
*/ */
export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnString): string;
export function bodyToHtml(content: IContent, highlights: string[], opts: IOptsReturnNode): ReactNode;
export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) { export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts = {}) {
const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body; const isHtmlMessage = content.format === "org.matrix.custom.html" && content.formatted_body;
let bodyHasEmoji = false; let bodyHasEmoji = false;
@ -501,7 +505,7 @@ export function bodyToHtml(content: IContent, highlights: string[], opts: IOpts
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
* @returns {string} Linkified string * @returns {string} Linkified string
*/ */
export function linkifyString(str: string, options = linkifyMatrix.options) { export function linkifyString(str: string, options = linkifyMatrix.options): string {
return _linkifyString(str, options); return _linkifyString(str, options);
} }
@ -512,7 +516,7 @@ export function linkifyString(str: string, options = linkifyMatrix.options) {
* @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options * @param {object} [options] Options for linkifyElement. Default: linkifyMatrix.options
* @returns {object} * @returns {object}
*/ */
export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options) { export function linkifyElement(element: HTMLElement, options = linkifyMatrix.options): HTMLElement {
return _linkifyElement(element, options); return _linkifyElement(element, options);
} }
@ -523,7 +527,7 @@ export function linkifyElement(element: HTMLElement, options = linkifyMatrix.opt
* @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options * @param {object} [options] Options for linkifyString. Default: linkifyMatrix.options
* @returns {string} * @returns {string}
*/ */
export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options) { export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatrix.options): string {
return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams); return sanitizeHtml(linkifyString(dirtyHtml, options), sanitizeHtmlParams);
} }
@ -534,7 +538,7 @@ export function linkifyAndSanitizeHtml(dirtyHtml: string, options = linkifyMatri
* @param {Node} node * @param {Node} node
* @returns {bool} * @returns {bool}
*/ */
export function checkBlockNode(node: Node) { export function checkBlockNode(node: Node): boolean {
switch (node.nodeName) { switch (node.nodeName) {
case "H1": case "H1":
case "H2": case "H2":

120
src/MediaDeviceHandler.ts Normal file
View file

@ -0,0 +1,120 @@
/*
Copyright 2017 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsStore from "./settings/SettingsStore";
import { SettingLevel } from "./settings/SettingLevel";
import { setMatrixCallAudioInput, setMatrixCallVideoInput } from "matrix-js-sdk/src/matrix";
import EventEmitter from 'events';
interface IMediaDevices {
audioOutput: Array<MediaDeviceInfo>;
audioInput: Array<MediaDeviceInfo>;
videoInput: Array<MediaDeviceInfo>;
}
export enum MediaDeviceHandlerEvent {
AudioOutputChanged = "audio_output_changed",
}
export default class MediaDeviceHandler extends EventEmitter {
private static internalInstance;
public static get instance(): MediaDeviceHandler {
if (!MediaDeviceHandler.internalInstance) {
MediaDeviceHandler.internalInstance = new MediaDeviceHandler();
}
return MediaDeviceHandler.internalInstance;
}
public static async hasAnyLabeledDevices(): Promise<boolean> {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.some(d => Boolean(d.label));
}
public static async getDevices(): Promise<IMediaDevices> {
// Only needed for Electron atm, though should work in modern browsers
// once permission has been granted to the webapp
try {
const devices = await navigator.mediaDevices.enumerateDevices();
const audioOutput = [];
const audioInput = [];
const videoInput = [];
devices.forEach((device) => {
switch (device.kind) {
case 'audiooutput': audioOutput.push(device); break;
case 'audioinput': audioInput.push(device); break;
case 'videoinput': videoInput.push(device); break;
}
});
return { audioOutput, audioInput, videoInput };
} catch (error) {
console.warn('Unable to refresh WebRTC Devices: ', error);
}
}
/**
* Retrieves devices from the SettingsStore and tells the js-sdk to use them
*/
public static loadDevices(): void {
const audioDeviceId = SettingsStore.getValue("webrtc_audioinput");
const videoDeviceId = SettingsStore.getValue("webrtc_videoinput");
setMatrixCallAudioInput(audioDeviceId);
setMatrixCallVideoInput(videoDeviceId);
}
public setAudioOutput(deviceId: string): void {
SettingsStore.setValue("webrtc_audiooutput", null, SettingLevel.DEVICE, deviceId);
this.emit(MediaDeviceHandlerEvent.AudioOutputChanged, deviceId);
}
/**
* This will not change the device that a potential call uses. The call will
* need to be ended and started again for this change to take effect
* @param {string} deviceId
*/
public setAudioInput(deviceId: string): void {
SettingsStore.setValue("webrtc_audioinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallAudioInput(deviceId);
}
/**
* This will not change the device that a potential call uses. The call will
* need to be ended and started again for this change to take effect
* @param {string} deviceId
*/
public setVideoInput(deviceId: string): void {
SettingsStore.setValue("webrtc_videoinput", null, SettingLevel.DEVICE, deviceId);
setMatrixCallVideoInput(deviceId);
}
public static getAudioOutput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audiooutput");
}
public static getAudioInput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_audioinput");
}
public static getVideoInput(): string {
return SettingsStore.getValueAt(SettingLevel.DEVICE, "webrtc_videoinput");
}
}

View file

@ -385,7 +385,7 @@ export class ModalManager {
</div> </div>
); );
ReactDOM.render(dialog, ModalManager.getOrCreateContainer()); setImmediate(() => ReactDOM.render(dialog, ModalManager.getOrCreateContainer()));
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015 - 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.
@ -14,7 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MatrixClientPeg} from './MatrixClientPeg'; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClientPeg } from './MatrixClientPeg';
/** /**
* Given a room object, return the alias we should use for it, * Given a room object, return the alias we should use for it,
@ -25,11 +27,11 @@ import {MatrixClientPeg} from './MatrixClientPeg';
* @param {Object} room The room object * @param {Object} room The room object
* @returns {string} A display alias for the given room * @returns {string} A display alias for the given room
*/ */
export function getDisplayAliasForRoom(room) { export function getDisplayAliasForRoom(room: Room): string {
return room.getCanonicalAlias() || room.getAltAliases()[0]; return room.getCanonicalAlias() || room.getAltAliases()[0];
} }
export function looksLikeDirectMessageRoom(room, myUserId) { export function looksLikeDirectMessageRoom(room: Room, myUserId: string): boolean {
const myMembership = room.getMyMembership(); const myMembership = room.getMyMembership();
const me = room.getMember(myUserId); const me = room.getMember(myUserId);
@ -48,7 +50,7 @@ export function looksLikeDirectMessageRoom(room, myUserId) {
return false; return false;
} }
export function guessAndSetDMRoom(room, isDirect) { export function guessAndSetDMRoom(room: Room, isDirect: boolean): Promise<void> {
let newTarget; let newTarget;
if (isDirect) { if (isDirect) {
const guessedUserId = guessDMRoomTargetId( const guessedUserId = guessDMRoomTargetId(
@ -70,7 +72,7 @@ export function guessAndSetDMRoom(room, isDirect) {
this room as a DM room this room as a DM room
* @returns {object} A promise * @returns {object} A promise
*/ */
export function setDMRoom(roomId, userId) { export function setDMRoom(roomId: string, userId: string): Promise<void> {
if (MatrixClientPeg.get().isGuest()) { if (MatrixClientPeg.get().isGuest()) {
return Promise.resolve(); return Promise.resolve();
} }
@ -114,7 +116,7 @@ export function setDMRoom(roomId, userId) {
* @param {string} myUserId User ID of the current user * @param {string} myUserId User ID of the current user
* @returns {string} User ID of the user that the room is probably a DM with * @returns {string} User ID of the user that the room is probably a DM with
*/ */
function guessDMRoomTargetId(room, myUserId) { function guessDMRoomTargetId(room: Room, myUserId: string): string {
let oldestTs; let oldestTs;
let oldestUser; let oldestUser;

View file

@ -468,7 +468,7 @@ function restoreEncryptionInfo(searchResultSlice = []) {
ev.event.curve25519Key, ev.event.curve25519Key,
ev.event.ed25519Key, ev.event.ed25519Key,
); );
ev._forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain; ev.forwardingCurve25519KeyChain = ev.event.forwardingCurve25519KeyChain;
delete ev.event.curve25519Key; delete ev.event.curve25519Key;
delete ev.event.ed25519Key; delete ev.event.ed25519Key;

View file

@ -17,8 +17,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import * as React from 'react'; import * as React from 'react';
import { User } from "matrix-js-sdk/src/models/user";
import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers'; import * as ContentHelpers from 'matrix-js-sdk/src/content-helpers';
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
@ -150,6 +150,10 @@ function success(promise?: Promise<any>) {
return {promise}; return {promise};
} }
function successSync(value: any) {
return success(Promise.resolve(value));
}
/* Disable the "unexpected this" error for these commands - all of the run /* Disable the "unexpected this" error for these commands - all of the run
* functions are called with `this` bound to the Command instance. * functions are called with `this` bound to the Command instance.
*/ */
@ -160,7 +164,7 @@ export const Commands = [
args: '<message>', args: '<message>',
description: _td('Sends the given message as a spoiler'), description: _td('Sends the given message as a spoiler'),
runFn: function(roomId, message) { runFn: function(roomId, message) {
return success(ContentHelpers.makeHtmlMessage( return successSync(ContentHelpers.makeHtmlMessage(
message, message,
`<span data-mx-spoiler>${message}</span>`, `<span data-mx-spoiler>${message}</span>`,
)); ));
@ -176,7 +180,7 @@ export const Commands = [
if (args) { if (args) {
message = message + ' ' + args; message = message + ' ' + args;
} }
return success(ContentHelpers.makeTextMessage(message)); return successSync(ContentHelpers.makeTextMessage(message));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -189,7 +193,7 @@ export const Commands = [
if (args) { if (args) {
message = message + ' ' + args; message = message + ' ' + args;
} }
return success(ContentHelpers.makeTextMessage(message)); return successSync(ContentHelpers.makeTextMessage(message));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -202,7 +206,7 @@ export const Commands = [
if (args) { if (args) {
message = message + ' ' + args; message = message + ' ' + args;
} }
return success(ContentHelpers.makeTextMessage(message)); return successSync(ContentHelpers.makeTextMessage(message));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -215,7 +219,7 @@ export const Commands = [
if (args) { if (args) {
message = message + ' ' + args; message = message + ' ' + args;
} }
return success(ContentHelpers.makeTextMessage(message)); return successSync(ContentHelpers.makeTextMessage(message));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -224,7 +228,7 @@ export const Commands = [
args: '<message>', args: '<message>',
description: _td('Sends a message as plain text, without interpreting it as markdown'), description: _td('Sends a message as plain text, without interpreting it as markdown'),
runFn: function(roomId, messages) { runFn: function(roomId, messages) {
return success(ContentHelpers.makeTextMessage(messages)); return successSync(ContentHelpers.makeTextMessage(messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -233,7 +237,7 @@ export const Commands = [
args: '<message>', args: '<message>',
description: _td('Sends a message as html, without interpreting it as markdown'), description: _td('Sends a message as html, without interpreting it as markdown'),
runFn: function(roomId, messages) { runFn: function(roomId, messages) {
return success(ContentHelpers.makeHtmlMessage(messages, messages)); return successSync(ContentHelpers.makeHtmlMessage(messages, messages));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -978,7 +982,7 @@ export const Commands = [
args: '<message>', args: '<message>',
runFn: function(roomId, args) { runFn: function(roomId, args) {
if (!args) return reject(this.getUserId()); if (!args) return reject(this.getUserId());
return success(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args))); return successSync(ContentHelpers.makeHtmlMessage(args, textToHtmlRainbow(args)));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -988,7 +992,7 @@ export const Commands = [
args: '<message>', args: '<message>',
runFn: function(roomId, args) { runFn: function(roomId, args) {
if (!args) return reject(this.getUserId()); if (!args) return reject(this.getUserId());
return success(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args))); return successSync(ContentHelpers.makeHtmlEmote(args, textToHtmlRainbow(args)));
}, },
category: CommandCategories.messages, category: CommandCategories.messages,
}), }),
@ -1015,9 +1019,8 @@ export const Commands = [
const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId); const member = MatrixClientPeg.get().getRoom(roomId).getMember(userId);
dis.dispatch<ViewUserPayload>({ dis.dispatch<ViewUserPayload>({
action: Action.ViewUser, action: Action.ViewUser,
// XXX: We should be using a real member object and not assuming what the // XXX: We should be using a real member object and not assuming what the receiver wants.
// receiver wants. member: member || { userId } as User,
member: member || {userId},
}); });
return success(); return success();
}, },

View file

@ -1,5 +1,5 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015 - 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.
@ -14,9 +14,13 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {MatrixClientPeg} from "./MatrixClientPeg"; import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType, MsgType } from "matrix-js-sdk/src/@types/event";
import { MatrixClientPeg } from "./MatrixClientPeg";
import shouldHideEvent from './shouldHideEvent'; import shouldHideEvent from './shouldHideEvent';
import {haveTileForEvent} from "./components/views/rooms/EventTile"; import { haveTileForEvent } from "./components/views/rooms/EventTile";
/** /**
* Returns true iff this event arriving in a room should affect the room's * Returns true iff this event arriving in a room should affect the room's
@ -25,28 +29,33 @@ import {haveTileForEvent} from "./components/views/rooms/EventTile";
* @param {Object} ev The event * @param {Object} ev The event
* @returns {boolean} True if the given event should affect the unread message count * @returns {boolean} True if the given event should affect the unread message count
*/ */
export function eventTriggersUnreadCount(ev) { export function eventTriggersUnreadCount(ev: MatrixEvent): boolean {
if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) { if (ev.sender && ev.sender.userId == MatrixClientPeg.get().credentials.userId) {
return false; return false;
} else if (ev.getType() == 'm.room.member') {
return false;
} else if (ev.getType() == 'm.room.third_party_invite') {
return false;
} else if (ev.getType() == 'm.call.answer' || ev.getType() == 'm.call.hangup') {
return false;
} else if (ev.getType() == 'm.room.message' && ev.getContent().msgtype == 'm.notify') {
return false;
} else if (ev.getType() == 'm.room.aliases' || ev.getType() == 'm.room.canonical_alias') {
return false;
} else if (ev.getType() == 'm.room.server_acl') {
return false;
} else if (ev.isRedacted()) {
return false;
} }
switch (ev.getType()) {
case EventType.RoomMember:
case EventType.RoomThirdPartyInvite:
case EventType.CallAnswer:
case EventType.CallHangup:
case EventType.RoomAliases:
case EventType.RoomCanonicalAlias:
case EventType.RoomServerAcl:
return false;
case EventType.RoomMessage:
if (ev.getContent().msgtype === MsgType.Notice) {
return false;
}
break;
}
if (ev.isRedacted()) return false;
return haveTileForEvent(ev); return haveTileForEvent(ev);
} }
export function doesRoomHaveUnreadMessages(room) { export function doesRoomHaveUnreadMessages(room: Room): boolean {
const myUserId = MatrixClientPeg.get().getUserId(); const myUserId = MatrixClientPeg.get().getUserId();
// get the most recent read receipt sent by our account. // get the most recent read receipt sent by our account.

View file

@ -57,6 +57,8 @@ export enum Modifiers {
// Meta-modifier: isMac ? CMD : CONTROL // Meta-modifier: isMac ? CMD : CONTROL
export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL; export const CMD_OR_CTRL = isMac ? Modifiers.COMMAND : Modifiers.CONTROL;
// Meta-key representing the digits [0-9] often found at the top of standard keyboard layouts
export const DIGITS = "digits";
interface IKeybind { interface IKeybind {
modifiers?: Modifiers[]; modifiers?: Modifiers[];
@ -319,6 +321,7 @@ const alternateKeyName: Record<string, string> = {
[Key.SPACE]: _td("Space"), [Key.SPACE]: _td("Space"),
[Key.HOME]: _td("Home"), [Key.HOME]: _td("Home"),
[Key.END]: _td("End"), [Key.END]: _td("End"),
[DIGITS]: _td("[number]"),
}; };
const keyIcon: Record<string, string> = { const keyIcon: Record<string, string> = {
[Key.ARROW_UP]: "↑", [Key.ARROW_UP]: "↑",

View file

@ -15,8 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {ReactElement} from 'react'; import { ReactElement } from 'react';
import Room from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import CommandProvider from './CommandProvider'; import CommandProvider from './CommandProvider';
import CommunityProvider from './CommunityProvider'; import CommunityProvider from './CommunityProvider';
import DuckDuckGoProvider from './DuckDuckGoProvider'; import DuckDuckGoProvider from './DuckDuckGoProvider';
@ -24,7 +25,7 @@ import RoomProvider from './RoomProvider';
import UserProvider from './UserProvider'; import UserProvider from './UserProvider';
import EmojiProvider from './EmojiProvider'; import EmojiProvider from './EmojiProvider';
import NotifProvider from './NotifProvider'; import NotifProvider from './NotifProvider';
import {timeout} from "../utils/promise"; import { timeout } from "../utils/promise";
import AutocompleteProvider, {ICommand} from "./AutocompleteProvider"; import AutocompleteProvider, {ICommand} from "./AutocompleteProvider";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import SpaceProvider from "./SpaceProvider"; import SpaceProvider from "./SpaceProvider";
@ -54,13 +55,14 @@ const PROVIDERS = [
EmojiProvider, EmojiProvider,
NotifProvider, NotifProvider,
CommandProvider, CommandProvider,
CommunityProvider,
DuckDuckGoProvider, DuckDuckGoProvider,
]; ];
// as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here // as the spaces feature is device configurable only, and toggling it refreshes the page, we can do this here
if (SettingsStore.getValue("feature_spaces")) { if (SettingsStore.getValue("feature_spaces")) {
PROVIDERS.push(SpaceProvider); PROVIDERS.push(SpaceProvider);
} else {
PROVIDERS.push(CommunityProvider);
} }
// Providers will get rejected if they take longer than this. // Providers will get rejected if they take longer than this.

View file

@ -15,7 +15,8 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import Room from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import {MatrixClientPeg} from '../MatrixClientPeg'; import {MatrixClientPeg} from '../MatrixClientPeg';

View file

@ -17,28 +17,24 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import {uniqBy, sortBy} from "lodash"; import { uniqBy, sortBy } from "lodash";
import Room from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {MatrixClientPeg} from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import {PillCompletion} from './Components'; import { PillCompletion } from './Components';
import {makeRoomPermalink} from "../utils/permalinks/Permalinks"; import { makeRoomPermalink } from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
import RoomAvatar from '../components/views/avatars/RoomAvatar'; import RoomAvatar from '../components/views/avatars/RoomAvatar';
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
const ROOM_REGEX = /\B#\S*/g; const ROOM_REGEX = /\B#\S*/g;
function score(query: string, space: string) { // Prefer canonical aliases over non-canonical ones
const index = space.indexOf(query); function canonicalScore(displayedAlias: string, room: Room): number {
if (index === -1) { return displayedAlias === room.getCanonicalAlias() ? 0 : 1;
return Infinity;
} else {
return index;
}
} }
function matcherObject(room: Room, displayedAlias: string, matchName = "") { function matcherObject(room: Room, displayedAlias: string, matchName = "") {
@ -106,7 +102,7 @@ export default class RoomProvider extends AutocompleteProvider {
const matchedString = command[0]; const matchedString = command[0];
completions = this.matcher.match(matchedString, limit); completions = this.matcher.match(matchedString, limit);
completions = sortBy(completions, [ completions = sortBy(completions, [
(c) => score(matchedString, c.displayedAlias), (c) => canonicalScore(c.displayedAlias, c.room),
(c) => c.displayedAlias.length, (c) => c.displayedAlias.length,
]); ]);
completions = uniqBy(completions, (match) => match.room); completions = uniqBy(completions, (match) => match.room);

View file

@ -20,19 +20,19 @@ limitations under the License.
import React from 'react'; import React from 'react';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
import AutocompleteProvider from './AutocompleteProvider'; import AutocompleteProvider from './AutocompleteProvider';
import {PillCompletion} from './Components'; import { PillCompletion } from './Components';
import * as sdk from '../index'; import * as sdk from '../index';
import QueryMatcher from './QueryMatcher'; import QueryMatcher from './QueryMatcher';
import {sortBy} from 'lodash'; import { sortBy } from 'lodash';
import {MatrixClientPeg} from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import MatrixEvent from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import Room from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import RoomMember from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import RoomState from "matrix-js-sdk/src/models/room-state"; import { RoomState } from "matrix-js-sdk/src/models/room-state";
import EventTimeline from "matrix-js-sdk/src/models/event-timeline"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import {makeUserPermalink} from "../utils/permalinks/Permalinks"; import { makeUserPermalink } from "../utils/permalinks/Permalinks";
import {ICompletion, ISelectionRange} from "./Autocompleter"; import { ICompletion, ISelectionRange } from "./Autocompleter";
const USER_REGEX = /\B@\S*/g; const USER_REGEX = /\B@\S*/g;

View file

@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React from "react"; import React, { HTMLAttributes } from "react";
interface IProps { interface IProps extends HTMLAttributes<HTMLDivElement> {
className?: string; className?: string;
onScroll?: () => void; onScroll?: () => void;
onWheel?: () => void; onWheel?: () => void;
@ -52,14 +52,18 @@ export default class AutoHideScrollbar extends React.Component<IProps> {
} }
public render() { public render() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { className, onScroll, onWheel, style, tabIndex, wrappedRef, children, ...otherProps } = this.props;
return (<div return (<div
{...otherProps}
ref={this.containerRef} ref={this.containerRef}
style={this.props.style} style={style}
className={["mx_AutoHideScrollbar", this.props.className].join(" ")} className={["mx_AutoHideScrollbar", className].join(" ")}
onWheel={this.props.onWheel} onWheel={onWheel}
tabIndex={this.props.tabIndex} tabIndex={tabIndex}
> >
{ this.props.children } { children }
</div>); </div>);
} }
} }

View file

@ -24,7 +24,6 @@ import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import { Droppable } from 'react-beautiful-dnd';
import classNames from 'classnames'; import classNames from 'classnames';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
@ -83,7 +82,7 @@ class GroupFilterPanel extends React.Component {
} }
}; };
onMouseDown = e => { onClick = e => {
// only dispatch if its not a no-op // only dispatch if its not a no-op
if (this.state.selectedTags.length > 0) { if (this.state.selectedTags.length > 0) {
dis.dispatch({action: 'deselect_tags'}); dis.dispatch({action: 'deselect_tags'});
@ -151,28 +150,15 @@ class GroupFilterPanel extends React.Component {
return <div className={classes} onClick={this.onClearFilterClick}> return <div className={classes} onClick={this.onClearFilterClick}>
<AutoHideScrollbar <AutoHideScrollbar
className="mx_GroupFilterPanel_scroller" className="mx_GroupFilterPanel_scroller"
// XXX: Use onMouseDown as a workaround for https://github.com/atlassian/react-beautiful-dnd/issues/273 onClick={this.onClick}
// instead of onClick. Otherwise we experience https://github.com/vector-im/element-web/issues/6253
onMouseDown={this.onMouseDown}
> >
<Droppable <div className="mx_GroupFilterPanel_tagTileContainer">
droppableId="tag-panel-droppable" { this.renderGlobalIcon() }
type="draggable-TagTile" { tags }
> <div>
{ (provided, snapshot) => ( { createButton }
<div </div>
className="mx_GroupFilterPanel_tagTileContainer" </div>
ref={provided.innerRef}
>
{ this.renderGlobalIcon() }
{ tags }
<div>
{createButton}
</div>
{ provided.placeholder }
</div>
) }
</Droppable>
</AutoHideScrollbar> </AutoHideScrollbar>
</div>; </div>;
} }

View file

@ -185,21 +185,24 @@ export default class IndicatorScrollbar extends React.Component {
}; };
render() { render() {
// eslint-disable-next-line no-unused-vars
const { children, trackHorizontalOverflow, verticalScrollsHorizontally, ...otherProps } = this.props;
const leftIndicatorStyle = {left: this.state.leftIndicatorOffset}; const leftIndicatorStyle = {left: this.state.leftIndicatorOffset};
const rightIndicatorStyle = {right: this.state.rightIndicatorOffset}; const rightIndicatorStyle = {right: this.state.rightIndicatorOffset};
const leftOverflowIndicator = this.props.trackHorizontalOverflow const leftOverflowIndicator = trackHorizontalOverflow
? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null; ? <div className="mx_IndicatorScrollbar_leftOverflowIndicator" style={leftIndicatorStyle} /> : null;
const rightOverflowIndicator = this.props.trackHorizontalOverflow const rightOverflowIndicator = trackHorizontalOverflow
? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null; ? <div className="mx_IndicatorScrollbar_rightOverflowIndicator" style={rightIndicatorStyle} /> : null;
return (<AutoHideScrollbar return (<AutoHideScrollbar
ref={this._collectScrollerComponent} ref={this._collectScrollerComponent}
wrappedRef={this._collectScroller} wrappedRef={this._collectScroller}
onWheel={this.onMouseWheel} onWheel={this.onMouseWheel}
{...this.props} {...otherProps}
> >
{ leftOverflowIndicator } { leftOverflowIndicator }
{ this.props.children } { children }
{ rightOverflowIndicator } { rightOverflowIndicator }
</AutoHideScrollbar>); </AutoHideScrollbar>);
} }

View file

@ -19,19 +19,16 @@ limitations under the License.
import * as React from 'react'; import * as React from 'react';
import * as PropTypes from 'prop-types'; import * as PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { DragDropContext } from 'react-beautiful-dnd';
import {Key} from '../../Keyboard'; import {Key} from '../../Keyboard';
import PageTypes from '../../PageTypes'; import PageTypes from '../../PageTypes';
import CallMediaHandler from '../../CallMediaHandler'; import MediaDeviceHandler from '../../MediaDeviceHandler';
import { fixupColorFonts } from '../../utils/FontManager'; import { fixupColorFonts } from '../../utils/FontManager';
import * as sdk from '../../index'; import * as sdk from '../../index';
import dis from '../../dispatcher/dispatcher'; import dis from '../../dispatcher/dispatcher';
import { IMatrixClientCreds } from '../../MatrixClientPeg'; import { IMatrixClientCreds } from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
import ResizeHandle from '../views/elements/ResizeHandle'; import ResizeHandle from '../views/elements/ResizeHandle';
import {Resizer, CollapseDistributor} from '../../resizer'; import {Resizer, CollapseDistributor} from '../../resizer';
import MatrixClientContext from "../../contexts/MatrixClientContext"; import MatrixClientContext from "../../contexts/MatrixClientContext";
@ -170,7 +167,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// stash the MatrixClient in case we log out before we are unmounted // stash the MatrixClient in case we log out before we are unmounted
this._matrixClient = this.props.matrixClient; this._matrixClient = this.props.matrixClient;
CallMediaHandler.loadDevices(); MediaDeviceHandler.loadDevices();
fixupColorFonts(); fixupColorFonts();
@ -569,50 +566,6 @@ class LoggedInView extends React.Component<IProps, IState> {
} }
}; };
_onDragEnd = (result) => {
// Dragged to an invalid destination, not onto a droppable
if (!result.destination) {
return;
}
const dest = result.destination.droppableId;
if (dest === 'tag-panel-droppable') {
// Could be "GroupTile +groupId:domain"
const draggableId = result.draggableId.split(' ').pop();
// Dispatch synchronously so that the GroupFilterPanel receives an
// optimistic update from GroupFilterOrderStore before the previous
// state is shown.
dis.dispatch(TagOrderActions.moveTag(
this._matrixClient,
draggableId,
result.destination.index,
), true);
} else if (dest.startsWith('room-sub-list-droppable_')) {
this._onRoomTileEndDrag(result);
}
};
_onRoomTileEndDrag = (result) => {
let newTag = result.destination.droppableId.split('_')[1];
let prevTag = result.source.droppableId.split('_')[1];
if (newTag === 'undefined') newTag = undefined;
if (prevTag === 'undefined') prevTag = undefined;
const roomId = result.draggableId.split('_')[1];
const oldIndex = result.source.index;
const newIndex = result.destination.index;
dis.dispatch(RoomListActions.tagRoom(
this._matrixClient,
this._matrixClient.getRoom(roomId),
prevTag, newTag,
oldIndex, newIndex,
), true);
};
render() { render() {
const RoomView = sdk.getComponent('structures.RoomView'); const RoomView = sdk.getComponent('structures.RoomView');
const UserView = sdk.getComponent('structures.UserView'); const UserView = sdk.getComponent('structures.UserView');
@ -679,17 +632,15 @@ class LoggedInView extends React.Component<IProps, IState> {
aria-hidden={this.props.hideToSRUsers} aria-hidden={this.props.hideToSRUsers}
> >
<ToastContainer /> <ToastContainer />
<DragDropContext onDragEnd={this._onDragEnd}> <div ref={this._resizeContainer} className={bodyClasses}>
<div ref={this._resizeContainer} className={bodyClasses}> { SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null }
{ SettingsStore.getValue("feature_spaces") ? <SpacePanel /> : null } <LeftPanel
<LeftPanel isMinimized={this.props.collapseLhs || false}
isMinimized={this.props.collapseLhs || false} resizeNotifier={this.props.resizeNotifier}
resizeNotifier={this.props.resizeNotifier} />
/> <ResizeHandle />
<ResizeHandle /> { pageElement }
{ pageElement } </div>
</div>
</DragDropContext>
</div> </div>
<CallContainer /> <CallContainer />
<NonUrgentToastContainer /> <NonUrgentToastContainer />

View file

@ -1461,7 +1461,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}); });
const dft = new DecryptionFailureTracker((total, errorCode) => { const dft = new DecryptionFailureTracker((total, errorCode) => {
Analytics.trackEvent('E2E', 'Decryption failure', errorCode, total); Analytics.trackEvent('E2E', 'Decryption failure', errorCode, String(total));
CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total }); CountlyAnalytics.instance.track("decryption_failure", { errorCode }, null, { sum: total });
}, (errorCode) => { }, (errorCode) => {
// Map JS-SDK error codes to tracker codes for aggregation // Map JS-SDK error codes to tracker codes for aggregation

View file

@ -82,8 +82,7 @@ export default class MyGroups extends React.Component {
</p> </p>
<p> <p>
{ _t( { _t(
"To set up a filter, drag a community avatar over to the filter panel on " + "You can click on an avatar in the " +
"the far left hand side of the screen. You can click on an avatar in the " +
"filter panel at any time to see only the rooms and people associated " + "filter panel at any time to see only the rooms and people associated " +
"with that community.", "with that community.",
) } ) }

View file

@ -337,11 +337,10 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
} }
private onRoomClicked = (room: IRoom, ev: ButtonEvent) => { private onRoomClicked = (room: IRoom, ev: ButtonEvent) => {
// If room was shift-clicked, remove it from the room directory
if (ev.shiftKey && !this.state.selectedCommunityId) { if (ev.shiftKey && !this.state.selectedCommunityId) {
ev.preventDefault(); ev.preventDefault();
this.removeFromDirectory(room); this.removeFromDirectory(room);
} else {
this.showRoom(room);
} }
}; };
@ -568,11 +567,11 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
let avatarUrl = null; let avatarUrl = null;
if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32); if (room.avatar_url) avatarUrl = mediaFromMxc(room.avatar_url).getSquareThumbnailHttp(32);
// We use onMouseDown instead of onClick, so that we can avoid text getting selected
return [ return [
<div key={ `${room.room_id}_avatar` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_avatar` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_roomAvatar" className="mx_RoomDirectory_roomAvatar"
> >
<BaseAvatar <BaseAvatar
@ -584,42 +583,50 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
url={avatarUrl} url={avatarUrl}
/> />
</div>, </div>,
<div key={ `${room.room_id}_description` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_description` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_roomDescription" className="mx_RoomDirectory_roomDescription"
> >
<div className="mx_RoomDirectory_name">{ name }</div>&nbsp; <div
<div className="mx_RoomDirectory_topic" className="mx_RoomDirectory_name"
onClick={ (ev) => { ev.stopPropagation(); } } onMouseDown={(ev) => this.onRoomClicked(room, ev)}
>
{ name }
</div>&nbsp;
<div
className="mx_RoomDirectory_topic"
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
dangerouslySetInnerHTML={{ __html: topic }} dangerouslySetInnerHTML={{ __html: topic }}
/> />
<div className="mx_RoomDirectory_alias">{ getDisplayAliasForRoom(room) }</div> <div
className="mx_RoomDirectory_alias"
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
>
{ getDisplayAliasForRoom(room) }
</div>
</div>, </div>,
<div key={ `${room.room_id}_memberCount` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_memberCount` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_roomMemberCount" className="mx_RoomDirectory_roomMemberCount"
> >
{ room.num_joined_members } { room.num_joined_members }
</div>, </div>,
<div key={ `${room.room_id}_preview` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_preview` }
onMouseDown={(ev) => this.onRoomClicked(room, ev)}
// cancel onMouseDown otherwise shift-clicking highlights text // cancel onMouseDown otherwise shift-clicking highlights text
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_preview" className="mx_RoomDirectory_preview"
> >
{previewButton} { previewButton }
</div>, </div>,
<div key={ `${room.room_id}_join` } <div
onClick={(ev) => this.onRoomClicked(room, ev)} key={ `${room.room_id}_join` }
// cancel onMouseDown otherwise shift-clicking highlights text onMouseDown={(ev) => this.onRoomClicked(room, ev)}
onMouseDown={(ev) => {ev.preventDefault();}}
className="mx_RoomDirectory_join" className="mx_RoomDirectory_join"
> >
{joinOrViewButton} { joinOrViewButton }
</div>, </div>,
]; ];
} }

View file

@ -41,7 +41,7 @@ export function getUnsentMessages(room) {
} }
@replaceableComponent("structures.RoomStatusBar") @replaceableComponent("structures.RoomStatusBar")
export default class RoomStatusBar extends React.Component { export default class RoomStatusBar extends React.PureComponent {
static propTypes = { static propTypes = {
// the room this statusbar is representing. // the room this statusbar is representing.
room: PropTypes.object.isRequired, room: PropTypes.object.isRequired,

View file

@ -23,8 +23,9 @@ limitations under the License.
import React, { createRef } from 'react'; import React, { createRef } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Room } from "matrix-js-sdk/src/models/room"; import { IRecommendedVersion, NotificationCountType, Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SearchResult } from "matrix-js-sdk/src/models/search-result";
import { EventSubscription } from "fbemitter"; import { EventSubscription } from "fbemitter";
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
@ -59,7 +60,7 @@ import ScrollPanel from "./ScrollPanel";
import TimelinePanel from "./TimelinePanel"; import TimelinePanel from "./TimelinePanel";
import ErrorBoundary from "../views/elements/ErrorBoundary"; import ErrorBoundary from "../views/elements/ErrorBoundary";
import RoomPreviewBar from "../views/rooms/RoomPreviewBar"; import RoomPreviewBar from "../views/rooms/RoomPreviewBar";
import SearchBar from "../views/rooms/SearchBar"; import SearchBar, { SearchScope } from "../views/rooms/SearchBar";
import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar"; import RoomUpgradeWarningBar from "../views/rooms/RoomUpgradeWarningBar";
import AuxPanel from "../views/rooms/AuxPanel"; import AuxPanel from "../views/rooms/AuxPanel";
import RoomHeader from "../views/rooms/RoomHeader"; import RoomHeader from "../views/rooms/RoomHeader";
@ -80,7 +81,6 @@ 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"; import { replaceableComponent } from "../../utils/replaceableComponent";
import { omit } from 'lodash';
import UIStore from "../../stores/UIStore"; import UIStore from "../../stores/UIStore";
const DEBUG = false; const DEBUG = false;
@ -139,11 +139,11 @@ export interface IState {
draggingFile: boolean; draggingFile: boolean;
searching: boolean; searching: boolean;
searchTerm?: string; searchTerm?: string;
searchScope?: "All" | "Room"; searchScope?: SearchScope;
searchResults?: XOR<{}, { searchResults?: XOR<{}, {
count: number; count: number;
highlights: string[]; highlights: string[];
results: MatrixEvent[]; results: SearchResult[];
next_batch: string; // eslint-disable-line camelcase next_batch: string; // eslint-disable-line camelcase
}>; }>;
searchHighlights?: string[]; searchHighlights?: string[];
@ -172,11 +172,7 @@ export interface IState {
// We load this later by asking the js-sdk to suggest a version for us. // We load this later by asking the js-sdk to suggest a version for us.
// This object is the result of Room#getRecommendedVersion() // This object is the result of Room#getRecommendedVersion()
upgradeRecommendation?: { upgradeRecommendation?: IRecommendedVersion;
version: string;
needsUpgrade: boolean;
urgent: boolean;
};
canReact: boolean; canReact: boolean;
canReply: boolean; canReply: boolean;
layout: Layout; layout: Layout;
@ -572,16 +568,12 @@ export default class RoomView extends React.Component<IProps, IState> {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const hasPropsDiff = objectHasDiff(this.props, nextProps); const hasPropsDiff = objectHasDiff(this.props, nextProps);
// React only shallow comparison and we only want to trigger const { upgradeRecommendation, ...state } = this.state;
// a component re-render if a room requires an upgrade const { upgradeRecommendation: newUpgradeRecommendation, ...newState } = nextState;
const newUpgradeRecommendation = nextState.upgradeRecommendation || {}
const state = omit(this.state, ['upgradeRecommendation']);
const newState = omit(nextState, ['upgradeRecommendation'])
const hasStateDiff = const hasStateDiff =
objectHasDiff(state, newState) || newUpgradeRecommendation?.needsUpgrade !== upgradeRecommendation?.needsUpgrade ||
(newUpgradeRecommendation.needsUpgrade === true) objectHasDiff(state, newState);
return hasPropsDiff || hasStateDiff; return hasPropsDiff || hasStateDiff;
} }
@ -701,16 +693,11 @@ export default class RoomView extends React.Component<IProps, IState> {
room_id: this.state.room.roomId, room_id: this.state.room.roomId,
event_id: this.state.initialEventId, event_id: this.state.initialEventId,
highlighted: false, highlighted: false,
replyingToEvent: this.state.replyToEvent,
}); });
} }
} }
private onLayoutChange = () => {
this.setState({
layout: SettingsStore.getValue("layout"),
});
};
private onRightPanelStoreUpdate = () => { private onRightPanelStoreUpdate = () => {
this.setState({ this.setState({
showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom, showRightPanel: RightPanelStore.getSharedInstance().isOpenForRoom,
@ -1276,7 +1263,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}); });
} }
private onSearch = (term: string, scope) => { private onSearch = (term: string, scope: SearchScope) => {
this.setState({ this.setState({
searchTerm: term, searchTerm: term,
searchScope: scope, searchScope: scope,
@ -1297,7 +1284,7 @@ export default class RoomView extends React.Component<IProps, IState> {
this.searchId = new Date().getTime(); this.searchId = new Date().getTime();
let roomId; let roomId;
if (scope === "Room") roomId = this.state.room.roomId; if (scope === SearchScope.Room) roomId = this.state.room.roomId;
debuglog("sending search request"); debuglog("sending search request");
const searchPromise = eventSearch(term, roomId); const searchPromise = eventSearch(term, roomId);
@ -1644,29 +1631,27 @@ export default class RoomView extends React.Component<IProps, IState> {
let auxPanelMaxHeight = UIStore.instance.windowHeight - let auxPanelMaxHeight = UIStore.instance.windowHeight -
(54 + // height of RoomHeader (54 + // height of RoomHeader
36 + // height of the status area 36 + // height of the status area
51 + // minimum height of the message compmoser 51 + // minimum height of the message composer
120); // amount of desired scrollback 120); // amount of desired scrollback
// XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway // XXX: this is a bit of a hack and might possibly cause the video to push out the page anyway
// but it's better than the video going missing entirely // but it's better than the video going missing entirely
if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50; if (auxPanelMaxHeight < 50) auxPanelMaxHeight = 50;
this.setState({auxPanelMaxHeight: auxPanelMaxHeight}); if (this.state.auxPanelMaxHeight !== auxPanelMaxHeight) {
this.setState({ auxPanelMaxHeight });
}
}; };
private onStatusBarVisible = () => { private onStatusBarVisible = () => {
if (this.unmounted) return; if (this.unmounted || this.state.statusBarVisible) return;
this.setState({ this.setState({ statusBarVisible: true });
statusBarVisible: true,
});
}; };
private onStatusBarHidden = () => { private onStatusBarHidden = () => {
// This is currently not desired as it is annoying if it keeps expanding and collapsing // This is currently not desired as it is annoying if it keeps expanding and collapsing
if (this.unmounted) return; if (this.unmounted || !this.state.statusBarVisible) return;
this.setState({ this.setState({ statusBarVisible: false });
statusBarVisible: false,
});
}; };
/** /**
@ -2069,7 +2054,7 @@ export default class RoomView extends React.Component<IProps, IState> {
if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) { if (!this.state.atEndOfLiveTimeline && !this.state.searchResults) {
const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton'); const JumpToBottomButton = sdk.getComponent('rooms.JumpToBottomButton');
jumpToBottom = (<JumpToBottomButton jumpToBottom = (<JumpToBottomButton
highlight={this.state.room.getUnreadNotificationCount('highlight') > 0} highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0}
numUnreadMessages={this.state.numUnreadMessages} numUnreadMessages={this.state.numUnreadMessages}
onScrollToBottomClick={this.jumpToLiveTimeline} onScrollToBottomClick={this.jumpToLiveTimeline}
roomId={this.state.roomId} roomId={this.state.roomId}

View file

@ -14,34 +14,34 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {ReactNode, useMemo, useState} from "react"; import React, { ReactNode, useMemo, useState } from "react";
import {Room} from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import {EventType, RoomType} from "matrix-js-sdk/src/@types/event"; import { EventType, RoomType } from "matrix-js-sdk/src/@types/event";
import classNames from "classnames"; import classNames from "classnames";
import {sortBy} from "lodash"; import { sortBy } from "lodash";
import {MatrixClientPeg} from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import {_t} from "../../languageHandler"; import { _t } from "../../languageHandler";
import AccessibleButton, {ButtonEvent} from "../views/elements/AccessibleButton"; import AccessibleButton, { ButtonEvent } from "../views/elements/AccessibleButton";
import BaseDialog from "../views/dialogs/BaseDialog"; import BaseDialog from "../views/dialogs/BaseDialog";
import Spinner from "../views/elements/Spinner"; import Spinner from "../views/elements/Spinner";
import SearchBox from "./SearchBox"; import SearchBox from "./SearchBox";
import RoomAvatar from "../views/avatars/RoomAvatar"; import RoomAvatar from "../views/avatars/RoomAvatar";
import RoomName from "../views/elements/RoomName"; import RoomName from "../views/elements/RoomName";
import {useAsyncMemo} from "../../hooks/useAsyncMemo"; import { useAsyncMemo } from "../../hooks/useAsyncMemo";
import {EnhancedMap} from "../../utils/maps"; import { EnhancedMap } from "../../utils/maps";
import StyledCheckbox from "../views/elements/StyledCheckbox"; import StyledCheckbox from "../views/elements/StyledCheckbox";
import AutoHideScrollbar from "./AutoHideScrollbar"; import AutoHideScrollbar from "./AutoHideScrollbar";
import BaseAvatar from "../views/avatars/BaseAvatar"; import BaseAvatar from "../views/avatars/BaseAvatar";
import {mediaFromMxc} from "../../customisations/Media"; import { mediaFromMxc } from "../../customisations/Media";
import InfoTooltip from "../views/elements/InfoTooltip"; import InfoTooltip from "../views/elements/InfoTooltip";
import TextWithTooltip from "../views/elements/TextWithTooltip"; import TextWithTooltip from "../views/elements/TextWithTooltip";
import {useStateToggle} from "../../hooks/useStateToggle"; import { useStateToggle } from "../../hooks/useStateToggle";
import {getOrder} from "../../stores/SpaceStore"; import { getChildOrder } from "../../stores/SpaceStore";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {linkifyElement} from "../../HtmlUtils"; import { linkifyElement } from "../../HtmlUtils";
interface IHierarchyProps { interface IHierarchyProps {
space: Room; space: Room;
@ -286,7 +286,7 @@ export const HierarchyLevel = ({
const children = Array.from(relations.get(spaceId)?.values() || []); const children = Array.from(relations.get(spaceId)?.values() || []);
const sortedChildren = sortBy(children, ev => { const sortedChildren = sortBy(children, ev => {
// XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting // XXX: Space Summary API doesn't give the child origin_server_ts but once it does we should use it for sorting
return getOrder(ev.content.order, null, ev.state_key); return getChildOrder(ev.content.order, null, ev.state_key);
}); });
const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => { const [subspaces, childRooms] = sortedChildren.reduce((result, ev: ISpaceSummaryEvent) => {
const roomId = ev.state_key; const roomId = ev.state_key;

View file

@ -59,7 +59,7 @@ import IconizedContextMenu, {
} from "../views/context_menus/IconizedContextMenu"; } from "../views/context_menus/IconizedContextMenu";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import {BetaPill} from "../views/beta/BetaCard"; import {BetaPill} from "../views/beta/BetaCard";
import {USER_LABS_TAB} from "../views/dialogs/UserSettingsDialog"; import { UserTab } from "../views/dialogs/UserSettingsDialog";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import Modal from "../../Modal"; import Modal from "../../Modal";
@ -166,7 +166,7 @@ const SpaceInfo = ({ space }) => {
const onBetaClick = () => { const onBetaClick = () => {
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB, initialTabId: UserTab.Labs,
}); });
}; };

View file

@ -18,14 +18,14 @@ limitations under the License.
*/ */
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import {LayoutPropType} from "../../settings/Layout"; import { LayoutPropType } from "../../settings/Layout";
import React, {createRef} from 'react'; import React, { createRef } from 'react';
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {EventTimeline} from "matrix-js-sdk/src/models/event-timeline"; import { EventTimeline } from "matrix-js-sdk/src/models/event-timeline";
import {TimelineWindow} from "matrix-js-sdk/src/timeline-window"; import { TimelineWindow } from "matrix-js-sdk/src/timeline-window";
import { _t } from '../../languageHandler'; import { _t } from '../../languageHandler';
import {MatrixClientPeg} from "../../MatrixClientPeg"; import { MatrixClientPeg } from "../../MatrixClientPeg";
import RoomContext from "../../contexts/RoomContext"; import RoomContext from "../../contexts/RoomContext";
import UserActivity from "../../UserActivity"; import UserActivity from "../../UserActivity";
import Modal from "../../Modal"; import Modal from "../../Modal";
@ -35,10 +35,11 @@ import { Key } from '../../Keyboard';
import Timer from '../../utils/Timer'; import Timer from '../../utils/Timer';
import shouldHideEvent from '../../shouldHideEvent'; import shouldHideEvent from '../../shouldHideEvent';
import EditorStateTransfer from '../../utils/EditorStateTransfer'; 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 {replaceableComponent} from "../../utils/replaceableComponent"; import { replaceableComponent } from "../../utils/replaceableComponent";
import { arrayFastClone } from "../../utils/arrays"; import { arrayFastClone } from "../../utils/arrays";
import { Action } from "../../dispatcher/actions";
const PAGINATE_SIZE = 20; const PAGINATE_SIZE = 20;
const INITIAL_SIZE = 20; const INITIAL_SIZE = 20;
@ -439,21 +440,42 @@ class TimelinePanel extends React.Component {
}; };
onAction = payload => { onAction = payload => {
if (payload.action === 'ignore_state_changed') { switch (payload.action) {
this.forceUpdate(); case "ignore_state_changed":
} this.forceUpdate();
if (payload.action === "edit_event") { break;
const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this.setState({editState}, () => { case "edit_event": {
if (payload.event && this._messagePanel.current) { const editState = payload.event ? new EditorStateTransfer(payload.event) : null;
this._messagePanel.current.scrollToEventIfNeeded( this.setState({editState}, () => {
payload.event.getId(), if (payload.event && this._messagePanel.current) {
); this._messagePanel.current.scrollToEventIfNeeded(
payload.event.getId(),
);
}
});
break;
}
case Action.ComposerInsert: {
// re-dispatch to the correct composer
if (this.state.editState) {
dis.dispatch({
...payload,
action: "edit_composer_insert",
});
} else {
dis.dispatch({
...payload,
action: "send_composer_insert",
});
} }
}); break;
} }
if (payload.action === "scroll_to_bottom") {
this.jumpToLiveTimeline(); case "scroll_to_bottom":
this.jumpToLiveTimeline();
break;
} }
}; };

View file

@ -26,7 +26,7 @@ import { ActionPayload } from "../../dispatcher/payloads";
import { Action } from "../../dispatcher/actions"; import { Action } from "../../dispatcher/actions";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
import { ContextMenuButton } from "./ContextMenu"; import { ContextMenuButton } from "./ContextMenu";
import { USER_NOTIFICATIONS_TAB, USER_SECURITY_TAB } from "../views/dialogs/UserSettingsDialog"; import { UserTab } from "../views/dialogs/UserSettingsDialog";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import FeedbackDialog from "../views/dialogs/FeedbackDialog"; import FeedbackDialog from "../views/dialogs/FeedbackDialog";
import Modal from "../../Modal"; import Modal from "../../Modal";
@ -408,12 +408,12 @@ export default class UserMenu extends React.Component<IProps, IState> {
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconBell" iconClassName="mx_UserMenu_iconBell"
label={_t("Notification settings")} label={_t("Notification settings")}
onClick={(e) => this.onSettingsOpen(e, USER_NOTIFICATIONS_TAB)} onClick={(e) => this.onSettingsOpen(e, UserTab.Notifications)}
/> />
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconLock" iconClassName="mx_UserMenu_iconLock"
label={_t("Security & privacy")} label={_t("Security & privacy")}
onClick={(e) => this.onSettingsOpen(e, USER_SECURITY_TAB)} onClick={(e) => this.onSettingsOpen(e, UserTab.Security)}
/> />
<IconizedContextMenuOption <IconizedContextMenuOption
iconClassName="mx_UserMenu_iconSettings" iconClassName="mx_UserMenu_iconSettings"

View file

@ -55,7 +55,7 @@ export default class ViewSource extends React.Component {
viewSourceContent() { viewSourceContent() {
const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit const mxEvent = this.props.mxEvent.replacingEvent() || this.props.mxEvent; // show the replacing event, not the original, if it is an edit
const isEncrypted = mxEvent.isEncrypted(); const isEncrypted = mxEvent.isEncrypted();
const decryptedEventSource = mxEvent._clearEvent; // FIXME: _clearEvent is private const decryptedEventSource = mxEvent.clearEvent; // FIXME: clearEvent is private
const originalEventSource = mxEvent.event; const originalEventSource = mxEvent.event;
if (isEncrypted) { if (isEncrypted) {

View file

@ -18,14 +18,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
} from '../../../stores/SetupEncryptionStore';
import SetupEncryptionBody from "./SetupEncryptionBody"; import SetupEncryptionBody from "./SetupEncryptionBody";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@ -61,18 +54,18 @@ export default class CompleteSecurity extends React.Component {
let icon; let icon;
let title; let title;
if (phase === PHASE_LOADING) { if (phase === Phase.Loading) {
return null; return null;
} else if (phase === PHASE_INTRO) { } 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) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />; icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_verified" />;
title = _t("Session verified"); title = _t("Session verified");
} else if (phase === PHASE_CONFIRM_SKIP) { } else if (phase === Phase.ConfirmSkip) {
icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />; icon = <span className="mx_CompleteSecurity_headerIcon mx_E2EIcon_warning" />;
title = _t("Are you sure?"); title = _t("Are you sure?");
} else if (phase === PHASE_BUSY) { } else if (phase === Phase.Busy) {
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 { } else {

View file

@ -269,7 +269,7 @@ export default class Registration extends React.Component<IProps, IState> {
); );
} }
private onUIAuthFinished = async (success, response, extra) => { private onUIAuthFinished = async (success: boolean, response: any) => {
if (!success) { if (!success) {
let msg = response.message || response.toString(); let msg = response.message || response.toString();
// can we give a better error message? // can we give a better error message?

View file

@ -21,15 +21,7 @@ import { MatrixClientPeg } from '../../../MatrixClientPeg';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog'; import VerificationRequestDialog from '../../views/dialogs/VerificationRequestDialog';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { import { SetupEncryptionStore, Phase } from '../../../stores/SetupEncryptionStore';
SetupEncryptionStore,
PHASE_LOADING,
PHASE_INTRO,
PHASE_BUSY,
PHASE_DONE,
PHASE_CONFIRM_SKIP,
PHASE_FINISHED,
} from '../../../stores/SetupEncryptionStore';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
function keyHasPassphrase(keyInfo) { function keyHasPassphrase(keyInfo) {
@ -63,7 +55,7 @@ export default class SetupEncryptionBody extends React.Component {
_onStoreUpdate = () => { _onStoreUpdate = () => {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
if (store.phase === PHASE_FINISHED) { if (store.phase === Phase.Finished) {
this.props.onFinished(); this.props.onFinished();
return; return;
} }
@ -136,7 +128,7 @@ export default class SetupEncryptionBody extends React.Component {
onClose={this.props.onFinished} onClose={this.props.onFinished}
member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)} member={MatrixClientPeg.get().getUser(this.state.verificationRequest.otherUserId)}
/>; />;
} else if (phase === PHASE_INTRO) { } else if (phase === Phase.Intro) {
const store = SetupEncryptionStore.sharedInstance(); const store = SetupEncryptionStore.sharedInstance();
let recoveryKeyPrompt; let recoveryKeyPrompt;
if (store.keyInfo && keyHasPassphrase(store.keyInfo)) { if (store.keyInfo && keyHasPassphrase(store.keyInfo)) {
@ -174,7 +166,7 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
</div> </div>
); );
} else if (phase === PHASE_DONE) { } else if (phase === Phase.Done) {
let message; let message;
if (this.state.backupInfo) { if (this.state.backupInfo) {
message = <p>{_t( message = <p>{_t(
@ -200,7 +192,7 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
</div> </div>
); );
} else if (phase === PHASE_CONFIRM_SKIP) { } else if (phase === Phase.ConfirmSkip) {
return ( return (
<div> <div>
<p>{_t( <p>{_t(
@ -224,7 +216,7 @@ export default class SetupEncryptionBody extends React.Component {
</div> </div>
</div> </div>
); );
} else if (phase === PHASE_BUSY || phase === PHASE_LOADING) { } 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

@ -17,16 +17,17 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useCallback, useContext, useEffect, useState} from 'react'; import React, { useCallback, useContext, useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import * as AvatarLogic from '../../../Avatar'; import * as AvatarLogic from '../../../Avatar';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import AccessibleButton from '../elements/AccessibleButton'; import AccessibleButton from '../elements/AccessibleButton';
import RoomContext from "../../../contexts/RoomContext"; import RoomContext from "../../../contexts/RoomContext";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {useEventEmitter} from "../../../hooks/useEventEmitter"; import { useEventEmitter } from "../../../hooks/useEventEmitter";
import {toPx} from "../../../utils/units"; import { toPx } from "../../../utils/units";
import {ResizeMethod} from "../../../Avatar";
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
interface IProps { interface IProps {

View file

@ -15,10 +15,11 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import BaseAvatar from './BaseAvatar'; import BaseAvatar from './BaseAvatar';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import {ResizeMethod} from "../../../Avatar";
export interface IProps { export interface IProps {
groupId?: string; groupId?: string;

View file

@ -16,14 +16,14 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import {RoomMember} from "matrix-js-sdk/src/models/room-member"; import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions"; import { Action } from "../../../dispatcher/actions";
import BaseAvatar from "./BaseAvatar"; import BaseAvatar from "./BaseAvatar";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
import {ResizeMethod} from "../../../Avatar";
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;

View file

@ -13,17 +13,17 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {ComponentProps} from 'react'; import React, { ComponentProps } from 'react';
import Room from 'matrix-js-sdk/src/models/room'; import { Room } from 'matrix-js-sdk/src/models/room';
import { ResizeMethod } from 'matrix-js-sdk/src/@types/partials';
import BaseAvatar from './BaseAvatar'; import BaseAvatar from './BaseAvatar';
import ImageView from '../elements/ImageView'; import ImageView from '../elements/ImageView';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; 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 { replaceableComponent } from "../../../utils/replaceableComponent";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media";
import {mediaFromMxc} from "../../../customisations/Media";
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,

View file

@ -25,6 +25,7 @@ import TextWithTooltip from "../elements/TextWithTooltip";
import Modal from "../../../Modal"; import Modal from "../../../Modal";
import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog"; import BetaFeedbackDialog from "../dialogs/BetaFeedbackDialog";
import SdkConfig from "../../../SdkConfig"; import SdkConfig from "../../../SdkConfig";
import SettingsFlag from "../elements/SettingsFlag";
interface IProps { interface IProps {
title?: string; title?: string;
@ -66,7 +67,7 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
const info = SettingsStore.getBetaInfo(featureId); const info = SettingsStore.getBetaInfo(featureId);
if (!info) return null; // Beta is invalid/disabled if (!info) return null; // Beta is invalid/disabled
const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading } = info; const { title, caption, disclaimer, image, feedbackLabel, feedbackSubheading, extraSettings } = info;
const value = SettingsStore.getValue(featureId); const value = SettingsStore.getValue(featureId);
let feedbackButton; let feedbackButton;
@ -82,26 +83,33 @@ const BetaCard = ({ title: titleOverride, featureId }: IProps) => {
} }
return <div className="mx_BetaCard"> return <div className="mx_BetaCard">
<div> <div className="mx_BetaCard_columns">
<h3 className="mx_BetaCard_title">
{ titleOverride || _t(title) }
<BetaPill />
</h3>
<span className="mx_BetaCard_caption">{ _t(caption) }</span>
<div> <div>
{ feedbackButton } <h3 className="mx_BetaCard_title">
<AccessibleButton { titleOverride || _t(title) }
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)} <BetaPill />
kind={feedbackButton ? "primary_outline" : "primary"} </h3>
> <span className="mx_BetaCard_caption">{ _t(caption) }</span>
{ value ? _t("Leave the beta") : _t("Join the beta") } <div className="mx_BetaCard_buttons">
</AccessibleButton> { feedbackButton }
<AccessibleButton
onClick={() => SettingsStore.setValue(featureId, null, SettingLevel.DEVICE, !value)}
kind={feedbackButton ? "primary_outline" : "primary"}
>
{ value ? _t("Leave the beta") : _t("Join the beta") }
</AccessibleButton>
</div>
{ disclaimer && <div className="mx_BetaCard_disclaimer">
{ disclaimer(value) }
</div> }
</div> </div>
{ disclaimer && <div className="mx_BetaCard_disclaimer"> <img src={image} alt="" />
{ disclaimer(value) }
</div> }
</div> </div>
<img src={image} alt="" /> { extraSettings && <div className="mx_BetaCard_relatedSettings">
{ extraSettings.map(key => (
<SettingsFlag key={key} name={key} level={SettingLevel.DEVICE} />
)) }
</div> }
</div>; </div>;
}; };

View file

@ -18,6 +18,7 @@ import React from 'react';
import { _t } from '../../../languageHandler'; 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 Field from "../elements/Field";
import Dialpad from '../voip/DialPad'; import Dialpad from '../voip/DialPad';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@ -44,13 +45,21 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.setState({value: this.state.value + digit}); this.setState({value: this.state.value + digit});
} }
onChange = (ev) => {
this.setState({value: ev.target.value});
}
render() { render() {
return <ContextMenu {...this.props}> return <ContextMenu {...this.props}>
<div className="mx_DialPadContextMenu_header"> <div className="mx_DialPadContextMenu_header">
<div> <div>
<span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span> <span className="mx_DialPadContextMenu_title">{_t("Dial pad")}</span>
</div> </div>
<div className="mx_DialPadContextMenu_dialled">{this.state.value}</div> <Field className="mx_DialPadContextMenu_dialled"
value={this.state.value} autoFocus={true}
onChange={this.onChange}
/>
</div> </div>
<div className="mx_DialPadContextMenu_horizSep" /> <div className="mx_DialPadContextMenu_horizSep" />
<div className="mx_DialPadContextMenu_dialPad"> <div className="mx_DialPadContextMenu_dialPad">

View file

@ -33,6 +33,7 @@ import { EventType } from "matrix-js-sdk/src/@types/event";
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard"; import { ReadPinsEventId } from "../right_panel/PinnedMessagesCard";
import ForwardDialog from "../dialogs/ForwardDialog"; import ForwardDialog from "../dialogs/ForwardDialog";
import { Action } from "../../../dispatcher/actions";
export function canCancel(eventStatus) { export function canCancel(eventStatus) {
return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT; return eventStatus === EventStatus.QUEUED || eventStatus === EventStatus.NOT_SENT;
@ -178,7 +179,7 @@ export default class MessageContextMenu extends React.Component {
pinnedIds.push(eventId); pinnedIds.push(eventId);
cli.setRoomAccountData(room.roomId, ReadPinsEventId, { cli.setRoomAccountData(room.roomId, ReadPinsEventId, {
event_ids: [ event_ids: [
...room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids, ...(room.getAccountData(ReadPinsEventId)?.getContent()?.event_ids || []),
eventId, eventId,
], ],
}); });
@ -200,7 +201,7 @@ export default class MessageContextMenu extends React.Component {
onQuoteClick = () => { onQuoteClick = () => {
dis.dispatch({ dis.dispatch({
action: 'quote', action: Action.ComposerInsert,
event: this.props.mxEvent, event: this.props.mxEvent,
}); });
this.closeMenu(); this.closeMenu();

View file

@ -23,45 +23,70 @@ import TagOrderActions from '../../../actions/TagOrderActions';
import {MenuItem} from "../../structures/ContextMenu"; import {MenuItem} from "../../structures/ContextMenu";
import MatrixClientContext from "../../../contexts/MatrixClientContext"; import MatrixClientContext from "../../../contexts/MatrixClientContext";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import GroupFilterOrderStore from "../../../stores/GroupFilterOrderStore";
@replaceableComponent("views.context_menus.TagTileContextMenu") @replaceableComponent("views.context_menus.TagTileContextMenu")
export default class TagTileContextMenu extends React.Component { export default class TagTileContextMenu extends React.Component {
static propTypes = { static propTypes = {
tag: PropTypes.string.isRequired, tag: PropTypes.string.isRequired,
index: PropTypes.number.isRequired,
/* callback called when the menu is dismissed */ /* callback called when the menu is dismissed */
onFinished: PropTypes.func.isRequired, onFinished: PropTypes.func.isRequired,
}; };
static contextType = MatrixClientContext; static contextType = MatrixClientContext;
constructor() { _onViewCommunityClick = () => {
super();
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
this._onRemoveClick = this._onRemoveClick.bind(this);
}
_onViewCommunityClick() {
dis.dispatch({ dis.dispatch({
action: 'view_group', action: 'view_group',
group_id: this.props.tag, group_id: this.props.tag,
}); });
this.props.onFinished(); this.props.onFinished();
} };
_onRemoveClick() { _onRemoveClick = () => {
dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag)); dis.dispatch(TagOrderActions.removeTag(this.context, this.props.tag));
this.props.onFinished(); this.props.onFinished();
} };
_onMoveUp = () => {
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index - 1));
this.props.onFinished();
};
_onMoveDown = () => {
dis.dispatch(TagOrderActions.moveTag(this.context, this.props.tag, this.props.index + 1));
this.props.onFinished();
};
render() { render() {
let moveUp;
let moveDown;
if (this.props.index > 0) {
moveUp = (
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveUp" onClick={this._onMoveUp}>
{ _t("Move up") }
</MenuItem>
);
}
if (this.props.index < (GroupFilterOrderStore.getOrderedTags() || []).length - 1) {
moveDown = (
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_moveDown" onClick={this._onMoveDown}>
{ _t("Move down") }
</MenuItem>
);
}
return <div> return <div>
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}> <MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_viewCommunity" onClick={this._onViewCommunityClick}>
{ _t('View Community') } { _t('View Community') }
</MenuItem> </MenuItem>
{ (moveUp || moveDown) ? <hr className="mx_TagTileContextMenu_separator" role="separator" /> : null }
{ moveUp }
{ moveDown }
<hr className="mx_TagTileContextMenu_separator" role="separator" /> <hr className="mx_TagTileContextMenu_separator" role="separator" />
<MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}> <MenuItem className="mx_TagTileContextMenu_item mx_TagTileContextMenu_hideCommunity" onClick={this._onRemoveClick}>
{ _t('Hide') } { _t("Unpin") }
</MenuItem> </MenuItem>
</div>; </div>;
} }

View file

@ -39,6 +39,9 @@ import ProgressBar from "../elements/ProgressBar";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import QueryMatcher from "../../../autocomplete/QueryMatcher"; import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -204,6 +207,17 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
setSelectedToAdd(new Set(selectedToAdd)); setSelectedToAdd(new Set(selectedToAdd));
} : null; } : null;
const [truncateAt, setTruncateAt] = useState(20);
function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)} />
);
}
return <div className="mx_AddExistingToSpace"> return <div className="mx_AddExistingToSpace">
<SearchBox <SearchBox
className="mx_textinput_icon mx_textinput_search" className="mx_textinput_icon mx_textinput_search"
@ -216,16 +230,21 @@ export const AddExistingToSpace: React.FC<IAddExistingToSpaceProps> = ({
{ rooms.length > 0 ? ( { rooms.length > 0 ? (
<div className="mx_AddExistingToSpace_section"> <div className="mx_AddExistingToSpace_section">
<h3>{ _t("Rooms") }</h3> <h3>{ _t("Rooms") }</h3>
{ rooms.map(room => { <TruncatedList
return <Entry truncateAt={truncateAt}
key={room.roomId} createOverflowElement={overflowTile}
room={room} getChildren={(start, end) => rooms.slice(start, end).map(room =>
checked={selectedToAdd.has(room)} <Entry
onChange={onChange ? (checked) => { key={room.roomId}
onChange(checked, room); room={room}
} : null} checked={selectedToAdd.has(room)}
/>; onChange={onChange ? (checked) => {
}) } onChange(checked, room);
} : null}
/>,
)}
getChildCount={() => rooms.length}
/>
</div> </div>
) : undefined } ) : undefined }

View file

@ -15,39 +15,41 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import {SettingLevel} from "../../../settings/SettingLevel"; import { SettingLevel } from "../../../settings/SettingLevel";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
unknownProfileUsers: Array<{
userId: string;
errorText: string;
}>;
onInviteAnyways: () => void;
onGiveUp: () => void;
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.AskInviteAnywayDialog") @replaceableComponent("views.dialogs.AskInviteAnywayDialog")
export default class AskInviteAnywayDialog extends React.Component { export default class AskInviteAnywayDialog extends React.Component<IProps> {
static propTypes = { private onInviteClicked = (): void => {
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
onInviteAnyways: PropTypes.func.isRequired,
onGiveUp: PropTypes.func.isRequired,
onFinished: PropTypes.func.isRequired,
};
_onInviteClicked = () => {
this.props.onInviteAnyways(); this.props.onInviteAnyways();
this.props.onFinished(true); this.props.onFinished(true);
}; };
_onInviteNeverWarnClicked = () => { private onInviteNeverWarnClicked = (): void => {
SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false); SettingsStore.setValue("promptBeforeInviteUnknownUsers", null, SettingLevel.ACCOUNT, false);
this.props.onInviteAnyways(); this.props.onInviteAnyways();
this.props.onFinished(true); this.props.onFinished(true);
}; };
_onGiveUpClicked = () => { private onGiveUpClicked = (): void => {
this.props.onGiveUp(); this.props.onGiveUp();
this.props.onFinished(false); this.props.onFinished(false);
}; };
render() { public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const errorList = this.props.unknownProfileUsers const errorList = this.props.unknownProfileUsers
@ -55,11 +57,12 @@ export default class AskInviteAnywayDialog extends React.Component {
return ( return (
<BaseDialog className='mx_RetryInvitesDialog' <BaseDialog className='mx_RetryInvitesDialog'
onFinished={this._onGiveUpClicked} onFinished={this.onGiveUpClicked}
title={_t('The following users may not exist')} title={_t('The following users may not exist')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >
<div id='mx_Dialog_content'> <div id='mx_Dialog_content'>
{/* eslint-disable-next-line */}
<p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p> <p>{_t("Unable to find profiles for the Matrix IDs listed below - would you like to invite them anyway?")}</p>
<ul> <ul>
{ errorList } { errorList }
@ -67,13 +70,13 @@ export default class AskInviteAnywayDialog extends React.Component {
</div> </div>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<button onClick={this._onGiveUpClicked}> <button onClick={this.onGiveUpClicked}>
{ _t('Close') } { _t('Close') }
</button> </button>
<button onClick={this._onInviteNeverWarnClicked}> <button onClick={this.onInviteNeverWarnClicked}>
{ _t('Invite anyway and never warn me again') } { _t('Invite anyway and never warn me again') }
</button> </button>
<button onClick={this._onInviteClicked} autoFocus={true}> <button onClick={this.onInviteClicked} autoFocus={true}>
{ _t('Invite anyway') } { _t('Invite anyway') }
</button> </button>
</div> </div>

View file

@ -29,7 +29,7 @@ import InfoDialog from "./InfoDialog";
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import {Action} from "../../../dispatcher/actions"; import {Action} from "../../../dispatcher/actions";
import {USER_LABS_TAB} from "./UserSettingsDialog"; import { UserTab } from "./UserSettingsDialog";
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
featureId: string; featureId: string;
@ -44,7 +44,12 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
const sendFeedback = async (ok: boolean) => { const sendFeedback = async (ok: boolean) => {
if (!ok) return onFinished(false); if (!ok) return onFinished(false);
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact); const extraData = SettingsStore.getBetaInfo(featureId)?.extraSettings.reduce((o, k) => {
o[k] = SettingsStore.getValue(k);
return o;
}, {});
submitFeedback(SdkConfig.get().bug_report_endpoint_url, info.feedbackLabel, comment, canContact, extraData);
onFinished(true); onFinished(true);
Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, { Modal.createTrackedDialog("Beta Dialog Sent", featureId, InfoDialog, {
@ -70,7 +75,7 @@ const BetaFeedbackDialog: React.FC<IProps> = ({featureId, onFinished}) => {
onFinished(false); onFinished(false);
defaultDispatcher.dispatch({ defaultDispatcher.dispatch({
action: Action.ViewUserSettings, action: Action.ViewUserSettings,
initialTabId: USER_LABS_TAB, initialTabId: UserTab.Labs,
}); });
}}> }}>
{ _t("To leave the beta, visit your settings.") } { _t("To leave the beta, visit your settings.") }

View file

@ -18,7 +18,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
@ -27,8 +26,27 @@ import sendBugReport, {downloadBugReport} from '../../../rageshake/submit-ragesh
import AccessibleButton from "../elements/AccessibleButton"; import AccessibleButton from "../elements/AccessibleButton";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
initialText?: string;
label?: string;
}
interface IState {
sendLogs: boolean;
busy: boolean;
err: string;
issueUrl: string;
text: string;
progress: string;
downloadBusy: boolean;
downloadProgress: string;
}
@replaceableComponent("views.dialogs.BugReportDialog") @replaceableComponent("views.dialogs.BugReportDialog")
export default class BugReportDialog extends React.Component { export default class BugReportDialog extends React.Component<IProps, IState> {
private unmounted: boolean;
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -41,25 +59,18 @@ export default class BugReportDialog extends React.Component {
downloadBusy: false, downloadBusy: false,
downloadProgress: null, downloadProgress: null,
}; };
this._unmounted = false; this.unmounted = false;
this._onSubmit = this._onSubmit.bind(this);
this._onCancel = this._onCancel.bind(this);
this._onTextChange = this._onTextChange.bind(this);
this._onIssueUrlChange = this._onIssueUrlChange.bind(this);
this._onSendLogsChange = this._onSendLogsChange.bind(this);
this._sendProgressCallback = this._sendProgressCallback.bind(this);
this._downloadProgressCallback = this._downloadProgressCallback.bind(this);
} }
componentWillUnmount() { public componentWillUnmount() {
this._unmounted = true; this.unmounted = true;
} }
_onCancel(ev) { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} }
_onSubmit(ev) { private onSubmit = (): void => {
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) { if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
this.setState({ this.setState({
err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."), err: _t("Please tell us what went wrong or, better, create a GitHub issue that describes the problem."),
@ -72,15 +83,15 @@ export default class BugReportDialog extends React.Component {
(this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given'); (this.state.issueUrl.length > 0 ? this.state.issueUrl : 'No issue link given');
this.setState({ busy: true, progress: null, err: null }); this.setState({ busy: true, progress: null, err: null });
this._sendProgressCallback(_t("Preparing to send logs")); this.sendProgressCallback(_t("Preparing to send logs"));
sendBugReport(SdkConfig.get().bug_report_endpoint_url, { sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
userText, userText,
sendLogs: true, sendLogs: true,
progressCallback: this._sendProgressCallback, progressCallback: this.sendProgressCallback,
label: this.props.label, label: this.props.label,
}).then(() => { }).then(() => {
if (!this._unmounted) { if (!this.unmounted) {
this.props.onFinished(false); this.props.onFinished(false);
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
// N.B. first param is passed to piwik and so doesn't want i18n // N.B. first param is passed to piwik and so doesn't want i18n
@ -91,7 +102,7 @@ export default class BugReportDialog extends React.Component {
}); });
} }
}, (err) => { }, (err) => {
if (!this._unmounted) { if (!this.unmounted) {
this.setState({ this.setState({
busy: false, busy: false,
progress: null, progress: null,
@ -101,14 +112,14 @@ export default class BugReportDialog extends React.Component {
}); });
} }
_onDownload = async (ev) => { private onDownload = async (): Promise<void> => {
this.setState({ downloadBusy: true }); this.setState({ downloadBusy: true });
this._downloadProgressCallback(_t("Preparing to download logs")); this.downloadProgressCallback(_t("Preparing to download logs"));
try { try {
await downloadBugReport({ await downloadBugReport({
sendLogs: true, sendLogs: true,
progressCallback: this._downloadProgressCallback, progressCallback: this.downloadProgressCallback,
label: this.props.label, label: this.props.label,
}); });
@ -117,7 +128,7 @@ export default class BugReportDialog extends React.Component {
downloadProgress: null, downloadProgress: null,
}); });
} catch (err) { } catch (err) {
if (!this._unmounted) { if (!this.unmounted) {
this.setState({ this.setState({
downloadBusy: false, downloadBusy: false,
downloadProgress: _t("Failed to send logs: ") + `${err.message}`, downloadProgress: _t("Failed to send logs: ") + `${err.message}`,
@ -126,33 +137,29 @@ export default class BugReportDialog extends React.Component {
} }
}; };
_onTextChange(ev) { private onTextChange = (ev: React.FormEvent<HTMLTextAreaElement>): void => {
this.setState({ text: ev.target.value }); this.setState({ text: ev.currentTarget.value });
} }
_onIssueUrlChange(ev) { private onIssueUrlChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.setState({ issueUrl: ev.target.value }); this.setState({ issueUrl: ev.currentTarget.value });
} }
_onSendLogsChange(ev) { private sendProgressCallback = (progress: string): void => {
this.setState({ sendLogs: ev.target.checked }); if (this.unmounted) {
}
_sendProgressCallback(progress) {
if (this._unmounted) {
return; return;
} }
this.setState({progress: progress}); this.setState({ progress });
} }
_downloadProgressCallback(downloadProgress) { private downloadProgressCallback = (downloadProgress: string): void => {
if (this._unmounted) { if (this.unmounted) {
return; return;
} }
this.setState({ downloadProgress }); this.setState({ downloadProgress });
} }
render() { public render() {
const Loader = sdk.getComponent("elements.Spinner"); const Loader = sdk.getComponent("elements.Spinner");
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -183,7 +190,7 @@ export default class BugReportDialog extends React.Component {
} }
return ( return (
<BaseDialog className="mx_BugReportDialog" onFinished={this._onCancel} <BaseDialog className="mx_BugReportDialog" onFinished={this.onCancel}
title={_t('Submit debug logs')} title={_t('Submit debug logs')}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
> >
@ -213,7 +220,7 @@ export default class BugReportDialog extends React.Component {
</b></p> </b></p>
<div className="mx_BugReportDialog_download"> <div className="mx_BugReportDialog_download">
<AccessibleButton onClick={this._onDownload} kind="link" disabled={this.state.downloadBusy}> <AccessibleButton onClick={this.onDownload} kind="link" disabled={this.state.downloadBusy}>
{ _t("Download logs") } { _t("Download logs") }
</AccessibleButton> </AccessibleButton>
{this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>} {this.state.downloadProgress && <span>{this.state.downloadProgress} ...</span>}
@ -223,7 +230,7 @@ export default class BugReportDialog extends React.Component {
type="text" type="text"
className="mx_BugReportDialog_field_input" className="mx_BugReportDialog_field_input"
label={_t("GitHub issue")} label={_t("GitHub issue")}
onChange={this._onIssueUrlChange} onChange={this.onIssueUrlChange}
value={this.state.issueUrl} value={this.state.issueUrl}
placeholder="https://github.com/vector-im/element-web/issues/..." placeholder="https://github.com/vector-im/element-web/issues/..."
/> />
@ -232,7 +239,7 @@ export default class BugReportDialog extends React.Component {
element="textarea" element="textarea"
label={_t("Notes")} label={_t("Notes")}
rows={5} rows={5}
onChange={this._onTextChange} onChange={this.onTextChange}
value={this.state.text} value={this.state.text}
placeholder={_t( placeholder={_t(
"If there is additional context that would help in " + "If there is additional context that would help in " +
@ -245,17 +252,12 @@ export default class BugReportDialog extends React.Component {
{error} {error}
</div> </div>
<DialogButtons primaryButton={_t("Send logs")} <DialogButtons primaryButton={_t("Send logs")}
onPrimaryButtonClick={this._onSubmit} onPrimaryButtonClick={this.onSubmit}
focus={true} focus={true}
onCancel={this._onCancel} onCancel={this.onCancel}
disabled={this.state.busy} disabled={this.state.busy}
/> />
</BaseDialog> </BaseDialog>
); );
} }
} }
BugReportDialog.propTypes = {
onFinished: PropTypes.func.isRequired,
initialText: PropTypes.string,
};

View file

@ -16,21 +16,26 @@ Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import request from 'browser-request'; import request from 'browser-request';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
interface IProps {
newVersion: string;
version: string;
onFinished: (success: boolean) => void;
}
const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk']; const REPOS = ['vector-im/element-web', 'matrix-org/matrix-react-sdk', 'matrix-org/matrix-js-sdk'];
export default class ChangelogDialog extends React.Component { export default class ChangelogDialog extends React.Component<IProps> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = {}; this.state = {};
} }
componentDidMount() { public componentDidMount() {
const version = this.props.newVersion.split('-'); const version = this.props.newVersion.split('-');
const version2 = this.props.version.split('-'); const version2 = this.props.version.split('-');
if (version == null || version2 == null) return; if (version == null || version2 == null) return;
@ -49,7 +54,7 @@ export default class ChangelogDialog extends React.Component {
} }
} }
_elementsForCommit(commit) { private elementsForCommit(commit): JSX.Element {
return ( return (
<li key={commit.sha} className="mx_ChangelogDialog_li"> <li key={commit.sha} className="mx_ChangelogDialog_li">
<a href={commit.html_url} target="_blank" rel="noreferrer noopener"> <a href={commit.html_url} target="_blank" rel="noreferrer noopener">
@ -59,7 +64,7 @@ export default class ChangelogDialog extends React.Component {
); );
} }
render() { public render() {
const Spinner = sdk.getComponent('views.elements.Spinner'); const Spinner = sdk.getComponent('views.elements.Spinner');
const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog'); const QuestionDialog = sdk.getComponent('dialogs.QuestionDialog');
@ -72,7 +77,7 @@ export default class ChangelogDialog extends React.Component {
msg: this.state[repo], msg: this.state[repo],
}); });
} else { } else {
content = this.state[repo].map(this._elementsForCommit); content = this.state[repo].map(this.elementsForCommit);
} }
return ( return (
<div key={repo}> <div key={repo}>
@ -99,9 +104,3 @@ export default class ChangelogDialog extends React.Component {
); );
} }
} }
ChangelogDialog.propTypes = {
version: PropTypes.string.isRequired,
newVersion: PropTypes.string.isRequired,
onFinished: PropTypes.func.isRequired,
};

View file

@ -17,7 +17,17 @@ limitations under the License.
import React from 'react'; import React from 'react';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
redact: () => Promise<void>;
onFinished: (success: boolean) => void;
}
interface IState {
isRedacting: boolean;
redactionErrorCode: string | number;
}
/* /*
* A dialog for confirming a redaction. * A dialog for confirming a redaction.
@ -32,7 +42,7 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
* To avoid this, we keep the dialog open as long as /redact is in progress. * To avoid this, we keep the dialog open as long as /redact is in progress.
*/ */
@replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog") @replaceableComponent("views.dialogs.ConfirmAndWaitRedactDialog")
export default class ConfirmAndWaitRedactDialog extends React.PureComponent { export default class ConfirmAndWaitRedactDialog extends React.PureComponent<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
@ -41,7 +51,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
}; };
} }
onParentFinished = async (proceed) => { public onParentFinished = async (proceed: boolean): Promise<void> => {
if (proceed) { if (proceed) {
this.setState({isRedacting: true}); this.setState({isRedacting: true});
try { try {
@ -60,7 +70,7 @@ export default class ConfirmAndWaitRedactDialog extends React.PureComponent {
} }
}; };
render() { public render() {
if (this.state.isRedacting) { if (this.state.isRedacting) {
if (this.state.redactionErrorCode) { if (this.state.redactionErrorCode) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");

View file

@ -19,11 +19,15 @@ import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
/* /*
* A dialog for confirming a redaction. * A dialog for confirming a redaction.
*/ */
@replaceableComponent("views.dialogs.ConfirmRedactDialog") @replaceableComponent("views.dialogs.ConfirmRedactDialog")
export default class ConfirmRedactDialog extends React.Component { export default class ConfirmRedactDialog extends React.Component<IProps> {
render() { render() {
const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog'); const TextInputDialog = sdk.getComponent('views.dialogs.TextInputDialog');
return ( return (

View file

@ -15,13 +15,31 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClient } from 'matrix-js-sdk/src/client'; import { MatrixClient } from 'matrix-js-sdk/src/client';
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import { GroupMemberType } from '../../../groups'; import { GroupMemberType } from '../../../groups';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
import {mediaFromMxc} from "../../../customisations/Media"; import { mediaFromMxc } from "../../../customisations/Media";
interface IProps {
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: RoomMember;
// group member object. Supply either this or 'member'
groupMember: GroupMemberType;
// needed if a group member is specified
matrixClient?: MatrixClient,
action: string; // eg. 'Ban'
title: string; // eg. 'Ban this user?'
// Whether to display a text field for a reason
// If true, the second argument to onFinished will
// be the string entered.
askReason?: boolean;
danger?: boolean;
onFinished: (success: boolean, reason?: string) => void;
}
/* /*
* A dialog for confirming an operation on another user. * A dialog for confirming an operation on another user.
@ -32,53 +50,23 @@ import {mediaFromMxc} from "../../../customisations/Media";
* Also tweaks the style for 'dangerous' actions (albeit only with colour) * Also tweaks the style for 'dangerous' actions (albeit only with colour)
*/ */
@replaceableComponent("views.dialogs.ConfirmUserActionDialog") @replaceableComponent("views.dialogs.ConfirmUserActionDialog")
export default class ConfirmUserActionDialog extends React.Component { export default class ConfirmUserActionDialog extends React.Component<IProps> {
static propTypes = { private reasonField: React.RefObject<HTMLInputElement> = React.createRef();
// matrix-js-sdk (room) member object. Supply either this or 'groupMember'
member: PropTypes.object,
// group member object. Supply either this or 'member'
groupMember: GroupMemberType,
// needed if a group member is specified
matrixClient: PropTypes.instanceOf(MatrixClient),
action: PropTypes.string.isRequired, // eg. 'Ban'
title: PropTypes.string.isRequired, // eg. 'Ban this user?'
// Whether to display a text field for a reason
// If true, the second argument to onFinished will
// be the string entered.
askReason: PropTypes.bool,
danger: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
};
static defaultProps = { static defaultProps = {
danger: false, danger: false,
askReason: false, askReason: false,
}; };
constructor(props) { public onOk = (): void => {
super(props); this.props.onFinished(true, this.reasonField.current?.value);
this._reasonField = null;
}
onOk = () => {
let reason;
if (this._reasonField) {
reason = this._reasonField.value;
}
this.props.onFinished(true, reason);
}; };
onCancel = () => { public onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
_collectReasonField = e => { public render() {
this._reasonField = e;
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar"); const MemberAvatar = sdk.getComponent("views.avatars.MemberAvatar");
@ -92,7 +80,7 @@ export default class ConfirmUserActionDialog extends React.Component {
<div> <div>
<form onSubmit={this.onOk}> <form onSubmit={this.onOk}>
<input className="mx_ConfirmUserActionDialog_reasonField" <input className="mx_ConfirmUserActionDialog_reasonField"
ref={this._collectReasonField} ref={this.reasonField}
placeholder={_t("Reason")} placeholder={_t("Reason")}
autoFocus={true} autoFocus={true}
/> />

View file

@ -15,22 +15,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import {_t} from "../../../languageHandler"; import {_t} from "../../../languageHandler";
import * as sdk from "../../../index"; import * as sdk from "../../../index";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog") interface IProps {
export default class ConfirmWipeDeviceDialog extends React.Component { onFinished: (success: boolean) => void;
static propTypes = { }
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => { @replaceableComponent("views.dialogs.ConfirmWipeDeviceDialog")
export default class ConfirmWipeDeviceDialog extends React.Component<IProps> {
private onConfirm = (): void => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
_onDecline = () => { private onDecline = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
@ -55,10 +54,10 @@ export default class ConfirmWipeDeviceDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Clear all data")} primaryButton={_t("Clear all data")}
onPrimaryButtonClick={this._onConfirm} onPrimaryButtonClick={this.onConfirm}
primaryButtonClass="danger" primaryButtonClass="danger"
cancelButton={_t("Cancel")} cancelButton={_t("Cancel")}
onCancel={this._onDecline} onCancel={this.onDecline}
/> />
</BaseDialog> </BaseDialog>
); );

View file

@ -15,44 +15,51 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import dis from '../../../dispatcher/dispatcher'; import dis from '../../../dispatcher/dispatcher';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {MatrixClientPeg} from '../../../MatrixClientPeg'; import {MatrixClientPeg} from '../../../MatrixClientPeg';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.CreateGroupDialog") interface IProps {
export default class CreateGroupDialog extends React.Component { onFinished: (success: boolean) => void;
static propTypes = { }
onFinished: PropTypes.func.isRequired,
};
state = { interface IState {
groupName: string;
groupId: string;
groupIdError: string;
creating: boolean;
createError: Error;
}
@replaceableComponent("views.dialogs.CreateGroupDialog")
export default class CreateGroupDialog extends React.Component<IProps, IState> {
public state = {
groupName: '', groupName: '',
groupId: '', groupId: '',
groupError: null, groupIdError: '',
creating: false, creating: false,
createError: null, createError: null,
}; };
_onGroupNameChange = e => { private onGroupNameChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
groupName: e.target.value, groupName: e.currentTarget.value,
}); });
}; };
_onGroupIdChange = e => { private onGroupIdChange = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
groupId: e.target.value, groupId: e.currentTarget.value,
}); });
}; };
_onGroupIdBlur = e => { private onGroupIdBlur = (): void => {
this._checkGroupId(); this.checkGroupId();
}; };
_checkGroupId(e) { private checkGroupId() {
let error = null; let error = null;
if (!this.state.groupId) { if (!this.state.groupId) {
error = _t("Community IDs cannot be empty."); error = _t("Community IDs cannot be empty.");
@ -67,12 +74,12 @@ export default class CreateGroupDialog extends React.Component {
return error; return error;
} }
_onFormSubmit = e => { private onFormSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); e.preventDefault();
if (this._checkGroupId()) return; if (this.checkGroupId()) return;
const profile = {}; const profile: any = {};
if (this.state.groupName !== '') { if (this.state.groupName !== '') {
profile.name = this.state.groupName; profile.name = this.state.groupName;
} }
@ -121,7 +128,7 @@ export default class CreateGroupDialog extends React.Component {
<BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished} <BaseDialog className="mx_CreateGroupDialog" onFinished={this.props.onFinished}
title={_t('Create Community')} title={_t('Create Community')}
> >
<form onSubmit={this._onFormSubmit}> <form onSubmit={this.onFormSubmit}>
<div className="mx_Dialog_content"> <div className="mx_Dialog_content">
<div className="mx_CreateGroupDialog_inputRow"> <div className="mx_CreateGroupDialog_inputRow">
<div className="mx_CreateGroupDialog_label"> <div className="mx_CreateGroupDialog_label">
@ -129,9 +136,9 @@ export default class CreateGroupDialog extends React.Component {
</div> </div>
<div> <div>
<input id="groupname" className="mx_CreateGroupDialog_input" <input id="groupname" className="mx_CreateGroupDialog_input"
autoFocus={true} size="64" autoFocus={true} size={64}
placeholder={_t('Example')} placeholder={_t('Example')}
onChange={this._onGroupNameChange} onChange={this.onGroupNameChange}
value={this.state.groupName} value={this.state.groupName}
/> />
</div> </div>
@ -144,10 +151,10 @@ export default class CreateGroupDialog extends React.Component {
<span className="mx_CreateGroupDialog_prefix">+</span> <span className="mx_CreateGroupDialog_prefix">+</span>
<input id="groupid" <input id="groupid"
className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix" className="mx_CreateGroupDialog_input mx_CreateGroupDialog_input_hasPrefixAndSuffix"
size="32" size={32}
placeholder={_t('example')} placeholder={_t('example')}
onChange={this._onGroupIdChange} onChange={this.onGroupIdChange}
onBlur={this._onGroupIdBlur} onBlur={this.onGroupIdBlur}
value={this.state.groupId} value={this.state.groupId}
/> />
<span className="mx_CreateGroupDialog_suffix"> <span className="mx_CreateGroupDialog_suffix">

View file

@ -22,7 +22,11 @@ import { _t } from '../../../languageHandler';
import SdkConfig from '../../../SdkConfig'; import SdkConfig from '../../../SdkConfig';
import Modal from '../../../Modal'; import Modal from '../../../Modal';
export default (props) => { interface IProps {
onFinished: (success: boolean) => void;
}
export default (props: IProps) => {
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
const _onLogoutClicked = () => { const _onLogoutClicked = () => {
@ -40,7 +44,7 @@ export default (props) => {
onFinished: (doLogout) => { onFinished: (doLogout) => {
if (doLogout) { if (doLogout) {
dis.dispatch({action: 'logout'}); dis.dispatch({action: 'logout'});
props.onFinished(); props.onFinished(true);
} }
}, },
}); });

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import Analytics from '../../../Analytics'; import Analytics from '../../../Analytics';
@ -28,8 +27,25 @@ import {DEFAULT_PHASE, PasswordAuthEntry, SSOAuthEntry} from "../auth/Interactiv
import StyledCheckbox from "../elements/StyledCheckbox"; import StyledCheckbox from "../elements/StyledCheckbox";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
interface IState {
shouldErase: boolean;
errStr: string;
authData: any; // for UIA
authEnabled: boolean; // see usages for information
// A few strings that are passed to InteractiveAuth for design or are displayed
// next to the InteractiveAuth component.
bodyText: string;
continueText: string;
continueKind: string;
}
@replaceableComponent("views.dialogs.DeactivateAccountDialog") @replaceableComponent("views.dialogs.DeactivateAccountDialog")
export default class DeactivateAccountDialog extends React.Component { export default class DeactivateAccountDialog extends React.Component<IProps, IState> {
constructor(props) { constructor(props) {
super(props); super(props);
@ -46,10 +62,10 @@ export default class DeactivateAccountDialog extends React.Component {
continueKind: null, continueKind: null,
}; };
this._initAuth(/* shouldErase= */false); this.initAuth(/* shouldErase= */false);
} }
_onStagePhaseChange = (stage, phase) => { private onStagePhaseChange = (stage: string, phase: string): void => {
const dialogAesthetics = { const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: { [SSOAuthEntry.PHASE_PREAUTH]: {
body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."), body: _t("Confirm your account deactivation by using Single Sign On to prove your identity."),
@ -87,19 +103,19 @@ export default class DeactivateAccountDialog extends React.Component {
this.setState({bodyText, continueText, continueKind}); this.setState({bodyText, continueText, continueKind});
}; };
_onUIAuthFinished = (success, result, extra) => { private onUIAuthFinished = (success: boolean, result: Error) => {
if (success) return; // great! makeRequest() will be called too. if (success) return; // great! makeRequest() will be called too.
if (result === ERROR_USER_CANCELLED) { if (result === ERROR_USER_CANCELLED) {
this._onCancel(); this.onCancel();
return; return;
} }
console.error("Error during UI Auth:", {result, extra}); console.error("Error during UI Auth:", { result });
this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")}); this.setState({errStr: _t("There was a problem communicating with the server. Please try again.")});
}; };
_onUIAuthComplete = (auth) => { private onUIAuthComplete = (auth: any): void => {
MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => { MatrixClientPeg.get().deactivateAccount(auth, this.state.shouldErase).then(r => {
// Deactivation worked - logout & close this dialog // Deactivation worked - logout & close this dialog
Analytics.trackEvent('Account', 'Deactivate Account'); Analytics.trackEvent('Account', 'Deactivate Account');
@ -111,9 +127,9 @@ export default class DeactivateAccountDialog extends React.Component {
}); });
}; };
_onEraseFieldChange = (ev) => { private onEraseFieldChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.setState({ this.setState({
shouldErase: ev.target.checked, shouldErase: ev.currentTarget.checked,
// Disable the auth form because we're going to have to reinitialize the auth // Disable the auth form because we're going to have to reinitialize the auth
// information. We do this because we can't modify the parameters in the UIA // information. We do this because we can't modify the parameters in the UIA
@ -123,14 +139,14 @@ export default class DeactivateAccountDialog extends React.Component {
}); });
// As mentioned above, set up for auth again to get updated UIA session info // As mentioned above, set up for auth again to get updated UIA session info
this._initAuth(/* shouldErase= */ev.target.checked); this.initAuth(/* shouldErase= */ev.currentTarget.checked);
}; };
_onCancel() { private onCancel(): void {
this.props.onFinished(false); this.props.onFinished(false);
} }
_initAuth(shouldErase) { private initAuth(shouldErase: boolean): void {
MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => { MatrixClientPeg.get().deactivateAccount(null, shouldErase).then(r => {
// If we got here, oops. The server didn't require any auth. // If we got here, oops. The server didn't require any auth.
// Our application lifecycle will catch the error and do the logout bits. // Our application lifecycle will catch the error and do the logout bits.
@ -148,7 +164,7 @@ export default class DeactivateAccountDialog extends React.Component {
}); });
} }
render() { public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
let error = null; let error = null;
@ -166,9 +182,9 @@ export default class DeactivateAccountDialog extends React.Component {
<InteractiveAuth <InteractiveAuth
matrixClient={MatrixClientPeg.get()} matrixClient={MatrixClientPeg.get()}
authData={this.state.authData} authData={this.state.authData}
makeRequest={this._onUIAuthComplete} makeRequest={this.onUIAuthComplete}
onAuthFinished={this._onUIAuthFinished} onAuthFinished={this.onUIAuthFinished}
onStagePhaseChange={this._onStagePhaseChange} onStagePhaseChange={this.onStagePhaseChange}
continueText={this.state.continueText} continueText={this.state.continueText}
continueKind={this.state.continueKind} continueKind={this.state.continueKind}
/> />
@ -214,7 +230,7 @@ export default class DeactivateAccountDialog extends React.Component {
<p> <p>
<StyledCheckbox <StyledCheckbox
checked={this.state.shouldErase} checked={this.state.shouldErase}
onChange={this._onEraseFieldChange} onChange={this.onEraseFieldChange}
> >
{_t( {_t(
"Please forget all messages I have sent when my account is deactivated " + "Please forget all messages I have sent when my account is deactivated " +
@ -235,7 +251,3 @@ export default class DeactivateAccountDialog extends React.Component {
); );
} }
} }
DeactivateAccountDialog.propTypes = {
onFinished: PropTypes.func.isRequired,
};

View file

@ -525,11 +525,11 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
} }
interface IAccountDataExplorerState { interface IAccountDataExplorerState {
[inputId: string]: boolean | string | any;
isRoomAccountData: boolean; isRoomAccountData: boolean;
event?: MatrixEvent; event?: MatrixEvent;
editing: boolean; editing: boolean;
queryEventType: string; queryEventType: string;
[inputId: string]: boolean | string;
} }
class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> { class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDataExplorerState> {

View file

@ -26,37 +26,37 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t } from '../../../languageHandler'; import { _t } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.dialogs.ErrorDialog") interface IProps {
export default class ErrorDialog extends React.Component { onFinished: (success: boolean) => void;
static propTypes = { title?: string;
title: PropTypes.string, description?: React.ReactNode;
description: PropTypes.oneOfType([ button?: string;
PropTypes.element, focus?: boolean;
PropTypes.string, headerImage?: string;
]), }
button: PropTypes.string,
focus: PropTypes.bool,
onFinished: PropTypes.func.isRequired,
headerImage: PropTypes.string,
};
static defaultProps = { interface IState {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.ErrorDialog")
export default class ErrorDialog extends React.Component<IProps, IState> {
public static defaultProps = {
focus: true, focus: true,
title: null, title: null,
description: null, description: null,
button: null, button: null,
}; };
onClick = () => { private onClick = () => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
render() { public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
return ( return (
<BaseDialog <BaseDialog

View file

@ -14,31 +14,35 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useMemo, useState, useEffect} from "react"; import React, { useMemo, useState, useEffect } from "react";
import classnames from "classnames"; import classnames from "classnames";
import {MatrixEvent} from "matrix-js-sdk/src/models/event"; import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import {Room} from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {_t} from "../../../languageHandler"; import { _t } from "../../../languageHandler";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import {useSettingValue, useFeatureEnabled} from "../../../hooks/useSettings"; import { useSettingValue, useFeatureEnabled } from "../../../hooks/useSettings";
import {UIFeature} from "../../../settings/UIFeature"; import { UIFeature } from "../../../settings/UIFeature";
import {Layout} from "../../../settings/Layout"; import { Layout } from "../../../settings/Layout";
import {IDialogProps} from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import {avatarUrlForUser} from "../../../Avatar"; import { avatarUrlForUser } from "../../../Avatar";
import EventTile from "../rooms/EventTile"; import EventTile from "../rooms/EventTile";
import SearchBox from "../../structures/SearchBox"; import SearchBox from "../../structures/SearchBox";
import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar"; import DecoratedRoomAvatar from "../avatars/DecoratedRoomAvatar";
import {Alignment} from '../elements/Tooltip'; import { Alignment } from '../elements/Tooltip';
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton"; import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import AutoHideScrollbar from "../../structures/AutoHideScrollbar"; import AutoHideScrollbar from "../../structures/AutoHideScrollbar";
import {StaticNotificationState} from "../../../stores/notifications/StaticNotificationState"; import { StaticNotificationState } from "../../../stores/notifications/StaticNotificationState";
import NotificationBadge from "../rooms/NotificationBadge"; import NotificationBadge from "../rooms/NotificationBadge";
import {RoomPermalinkCreator} from "../../../utils/permalinks/Permalinks"; import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import {sortRooms} from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm"; import { sortRooms } from "../../../stores/room-list/algorithms/tag-sorting/RecentAlgorithm";
import QueryMatcher from "../../../autocomplete/QueryMatcher"; import QueryMatcher from "../../../autocomplete/QueryMatcher";
import TruncatedList from "../elements/TruncatedList";
import EntityTile from "../rooms/EntityTile";
import BaseAvatar from "../avatars/BaseAvatar";
const AVATAR_SIZE = 30; const AVATAR_SIZE = 30;
@ -162,6 +166,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}); });
mockEvent.sender = { mockEvent.sender = {
name: profileInfo.displayname || userId, name: profileInfo.displayname || userId,
rawDisplayName: profileInfo.displayname,
userId, userId,
getAvatarUrl: (..._) => { getAvatarUrl: (..._) => {
return avatarUrlForUser( return avatarUrlForUser(
@ -170,7 +175,7 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
); );
}, },
getMxcAvatarUrl: () => profileInfo.avatar_url, getMxcAvatarUrl: () => profileInfo.avatar_url,
}; } as RoomMember;
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
const lcQuery = query.toLowerCase(); const lcQuery = query.toLowerCase();
@ -194,6 +199,17 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
}).match(lcQuery); }).match(lcQuery);
} }
const [truncateAt, setTruncateAt] = useState(20);
function overflowTile(overflowCount, totalCount) {
const text = _t("and %(count)s others...", { count: overflowCount });
return (
<EntityTile className="mx_EntityTile_ellipsis" avatarJsx={
<BaseAvatar url={require("../../../../res/img/ellipsis.svg")} name="..." width={36} height={36} />
} name={text} presenceState="online" suppressOnHover={true}
onClick={() => setTruncateAt(totalCount)} />
);
}
return <BaseDialog return <BaseDialog
title={_t("Forward message")} title={_t("Forward message")}
className="mx_ForwardDialog" className="mx_ForwardDialog"
@ -226,15 +242,20 @@ const ForwardDialog: React.FC<IProps> = ({ matrixClient: cli, event, permalinkCr
<AutoHideScrollbar className="mx_ForwardList_content"> <AutoHideScrollbar className="mx_ForwardList_content">
{ rooms.length > 0 ? ( { rooms.length > 0 ? (
<div className="mx_ForwardList_results"> <div className="mx_ForwardList_results">
{ rooms.map(room => <TruncatedList
<Entry truncateAt={truncateAt}
key={room.roomId} createOverflowElement={overflowTile}
room={room} getChildren={(start, end) => rooms.slice(start, end).map(room =>
event={event} <Entry
matrixClient={cli} key={room.roomId}
onFinished={onFinished} room={room}
/>, event={event}
) } matrixClient={cli}
onFinished={onFinished}
/>,
)}
getChildCount={() => rooms.length}
/>
</div> </div>
) : <span className="mx_ForwardList_noResults"> ) : <span className="mx_ForwardList_noResults">
{ _t("No results") } { _t("No results") }

View file

@ -158,8 +158,8 @@ class ThreepidMember extends Member {
} }
interface IDMUserTileProps { interface IDMUserTileProps {
member: RoomMember; member: Member;
onRemove(member: RoomMember): void; onRemove(member: Member): void;
} }
class DMUserTile extends React.PureComponent<IDMUserTileProps> { class DMUserTile extends React.PureComponent<IDMUserTileProps> {
@ -173,7 +173,7 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
render() { render() {
const avatarSize = 20; const avatarSize = 20;
const avatar = this.props.member.isEmail const avatar = (this.props.member as ThreepidMember).isEmail
? <img ? <img
className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar' className='mx_InviteDialog_userTile_avatar mx_InviteDialog_userTile_threepidAvatar'
src={require("../../../../res/img/icon-email-pill-avatar.svg")} src={require("../../../../res/img/icon-email-pill-avatar.svg")}
@ -215,9 +215,9 @@ class DMUserTile extends React.PureComponent<IDMUserTileProps> {
} }
interface IDMRoomTileProps { interface IDMRoomTileProps {
member: RoomMember; member: Member;
lastActiveTs: number; lastActiveTs: number;
onToggle(member: RoomMember): void; onToggle(member: Member): void;
highlightWord: string; highlightWord: string;
isSelected: boolean; isSelected: boolean;
} }
@ -275,7 +275,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
} }
const avatarSize = 36; const avatarSize = 36;
const avatar = this.props.member.isEmail const avatar = (this.props.member as ThreepidMember).isEmail
? <img ? <img
src={require("../../../../res/img/icon-email-pill-avatar.svg")} src={require("../../../../res/img/icon-email-pill-avatar.svg")}
width={avatarSize} height={avatarSize} /> width={avatarSize} height={avatarSize} />
@ -303,7 +303,7 @@ class DMRoomTile extends React.PureComponent<IDMRoomTileProps> {
</span> </span>
); );
const caption = this.props.member.isEmail const caption = (this.props.member as ThreepidMember).isEmail
? _t("Invite by email") ? _t("Invite by email")
: this.highlightName(this.props.member.userId); : this.highlightName(this.props.member.userId);
@ -339,7 +339,7 @@ interface IInviteDialogProps {
} }
interface IInviteDialogState { interface IInviteDialogState {
targets: RoomMember[]; // array of Member objects (see interface above) targets: Member[]; // array of Member objects (see interface above)
filterText: string; filterText: string;
recents: { user: Member, userId: string }[]; recents: { user: Member, userId: string }[];
numRecentsShown: number; numRecentsShown: number;

View file

@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef(); private appFrame: React.RefObject<HTMLIFrameElement> = React.createRef();
state: IState = { state: IState = {
disabledButtonIds: [], disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled)
.map(b => b.id),
}; };
constructor(props) { constructor(props) {

View file

@ -1,149 +0,0 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {PureComponent} from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import PropTypes from "prop-types";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
import {replaceableComponent} from "../../../utils/replaceableComponent";
/*
* A dialog for reporting an event.
*/
@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends PureComponent {
static propTypes = {
mxEvent: PropTypes.instanceOf(MatrixEvent).isRequired,
onFinished: PropTypes.func.isRequired,
};
constructor(props) {
super(props);
this.state = {
reason: "",
busy: false,
err: null,
};
}
_onReasonChange = ({target: {value: reason}}) => {
this.setState({ reason });
};
_onCancel = () => {
this.props.onFinished(false);
};
_onSubmit = async () => {
if (!this.state.reason || !this.state.reason.trim()) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
this.setState({
busy: true,
err: null,
});
try {
const ev = this.props.mxEvent;
await MatrixClientPeg.get().reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
this.props.onFinished(true);
} catch (e) {
this.setState({
busy: false,
err: e.message,
});
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Loader = sdk.getComponent('elements.Spinner');
const Field = sdk.getComponent('elements.Field');
let error = null;
if (this.state.err) {
error = <div className="error">
{this.state.err}
</div>;
}
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
<Loader />
</div>
);
}
const adminMessageMD =
SdkConfig.get().reportEvent &&
SdkConfig.get().reportEvent.adminMessageMD;
let adminMessage;
if (adminMessageMD) {
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
}
return (
<BaseDialog
className="mx_BugReportDialog"
onFinished={this.props.onFinished}
title={_t('Report Content to Your Homeserver Administrator')}
contentId='mx_ReportEventDialog'
>
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
<p>
{
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
"your homeserver. If messages in this room are encrypted, your homeserver " +
"administrator will not be able to read the message text or view any files or images.")
}
</p>
{adminMessage}
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this._onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this._onSubmit}
focus={true}
onCancel={this._onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View file

@ -0,0 +1,445 @@
/*
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import { _t } from '../../../languageHandler';
import { ensureDMExists } from "../../../createRoom";
import { IDialogProps } from "./IDialogProps";
import {MatrixEvent} from "matrix-js-sdk/src/models/event";
import {MatrixClientPeg} from "../../../MatrixClientPeg";
import SdkConfig from '../../../SdkConfig';
import Markdown from '../../../Markdown';
import {replaceableComponent} from "../../../utils/replaceableComponent";
import SettingsStore from "../../../settings/SettingsStore";
import StyledRadioButton from "../elements/StyledRadioButton";
interface IProps extends IDialogProps {
mxEvent: MatrixEvent;
}
interface IState {
// A free-form text describing the abuse.
reason: string;
busy: boolean;
err?: string;
// If we know it, the nature of the abuse, as specified by MSC3215.
nature?: EXTENDED_NATURE;
}
const MODERATED_BY_STATE_EVENT_TYPE = [
"org.matrix.msc3215.room.moderation.moderated_by",
/**
* Unprefixed state event. Not ready for prime time.
*
* "m.room.moderation.moderated_by"
*/
];
const ABUSE_EVENT_TYPE = "org.matrix.msc3215.abuse.report";
// Standard abuse natures.
enum NATURE {
DISAGREEMENT = "org.matrix.msc3215.abuse.nature.disagreement",
TOXIC = "org.matrix.msc3215.abuse.nature.toxic",
ILLEGAL = "org.matrix.msc3215.abuse.nature.illegal",
SPAM = "org.matrix.msc3215.abuse.nature.spam",
OTHER = "org.matrix.msc3215.abuse.nature.other",
}
enum NON_STANDARD_NATURE {
// Non-standard abuse nature.
// It should never leave the client - we use it to fallback to
// server-wide abuse reporting.
ADMIN = "non-standard.abuse.nature.admin"
}
type EXTENDED_NATURE = NATURE | NON_STANDARD_NATURE;
type Moderation = {
// The id of the moderation room.
moderationRoomId: string;
// The id of the bot in charge of forwarding abuse reports to the moderation room.
moderationBotUserId: string;
}
/*
* A dialog for reporting an event.
*
* The actual content of the dialog will depend on two things:
*
* 1. Is `feature_report_to_moderators` enabled?
* 2. Does the room support moderation as per MSC3215, i.e. is there
* a well-formed state event `m.room.moderation.moderated_by`
* /`org.matrix.msc3215.room.moderation.moderated_by`?
*/
@replaceableComponent("views.dialogs.ReportEventDialog")
export default class ReportEventDialog extends React.Component<IProps, IState> {
// If the room supports moderation, the moderation information.
private moderation?: Moderation;
constructor(props: IProps) {
super(props);
let moderatedByRoomId = null;
let moderatedByUserId = null;
if (SettingsStore.getValue("feature_report_to_moderators")) {
// The client supports reporting to moderators.
// Does the room support it, too?
// Extract state events to determine whether we should display
const client = MatrixClientPeg.get();
const room = client.getRoom(props.mxEvent.getRoomId());
for (const stateEventType of MODERATED_BY_STATE_EVENT_TYPE) {
const stateEvent = room.currentState.getStateEvents(stateEventType, stateEventType);
if (!stateEvent) {
continue;
}
if (Array.isArray(stateEvent)) {
// Internal error.
throw new TypeError(`getStateEvents(${stateEventType}, ${stateEventType}) ` +
"should return at most one state event");
}
const event = stateEvent.event;
if (!("content" in event) || typeof event["content"] != "object") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug("Moderation error", "state event", stateEventType,
"should have an object field `content`, got", event);
continue;
}
const content = event["content"];
if (!("room_id" in content) || typeof content["room_id"] != "string") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug("Moderation error", "state event", stateEventType,
"should have a string field `content.room_id`, got", event);
continue;
}
if (!("user_id" in content) || typeof content["user_id"] != "string") {
// The room is improperly configured.
// Display this debug message for the sake of moderators.
console.debug("Moderation error", "state event", stateEventType,
"should have a string field `content.user_id`, got", event);
continue;
}
moderatedByRoomId = content["room_id"];
moderatedByUserId = content["user_id"];
}
if (moderatedByRoomId && moderatedByUserId) {
// The room supports moderation.
this.moderation = {
moderationRoomId: moderatedByRoomId,
moderationBotUserId: moderatedByUserId,
};
}
}
this.state = {
// A free-form text describing the abuse.
reason: "",
busy: false,
err: null,
// If specified, the nature of the abuse, as specified by MSC3215.
nature: null,
};
}
// The user has written down a freeform description of the abuse.
private onReasonChange = ({target: {value: reason}}): void => {
this.setState({ reason });
};
// The user has clicked on a nature.
private onNatureChosen = (e: React.FormEvent<HTMLInputElement>): void => {
this.setState({ nature: e.currentTarget.value as EXTENDED_NATURE});
};
// The user has clicked "cancel".
private onCancel = (): void => {
this.props.onFinished(false);
};
// The user has clicked "submit".
private onSubmit = async () => {
let reason = this.state.reason || "";
reason = reason.trim();
if (this.moderation) {
// This room supports moderation.
// We need a nature.
// If the nature is `NATURE.OTHER` or `NON_STANDARD_NATURE.ADMIN`, we also need a `reason`.
if (!this.state.nature ||
((this.state.nature == NATURE.OTHER || this.state.nature == NON_STANDARD_NATURE.ADMIN)
&& !reason)
) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
} else {
// This room does not support moderation.
// We need a `reason`.
if (!reason) {
this.setState({
err: _t("Please fill why you're reporting."),
});
return;
}
}
this.setState({
busy: true,
err: null,
});
try {
const client = MatrixClientPeg.get();
const ev = this.props.mxEvent;
if (this.moderation && this.state.nature != NON_STANDARD_NATURE.ADMIN) {
const nature: NATURE = this.state.nature;
// Report to moderators through to the dedicated bot,
// as configured in the room's state events.
const dmRoomId = await ensureDMExists(client, this.moderation.moderationBotUserId);
await client.sendEvent(dmRoomId, ABUSE_EVENT_TYPE, {
event_id: ev.getId(),
room_id: ev.getRoomId(),
moderated_by_id: this.moderation.moderationRoomId,
nature,
reporter: client.getUserId(),
comment: this.state.reason.trim(),
});
} else {
// Report to homeserver admin through the dedicated Matrix API.
await client.reportEvent(ev.getRoomId(), ev.getId(), -100, this.state.reason.trim());
}
this.props.onFinished(true);
} catch (e) {
this.setState({
busy: false,
err: e.message,
});
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
const Loader = sdk.getComponent('elements.Spinner');
const Field = sdk.getComponent('elements.Field');
let error = null;
if (this.state.err) {
error = <div className="error">
{this.state.err}
</div>;
}
let progress = null;
if (this.state.busy) {
progress = (
<div className="progress">
<Loader />
</div>
);
}
const adminMessageMD =
SdkConfig.get().reportEvent &&
SdkConfig.get().reportEvent.adminMessageMD;
let adminMessage;
if (adminMessageMD) {
const html = new Markdown(adminMessageMD).toHTML({ externalLinks: true });
adminMessage = <p dangerouslySetInnerHTML={{ __html: html }} />;
}
if (this.moderation) {
// Display report-to-moderator dialog.
// We let the user pick a nature.
const client = MatrixClientPeg.get();
const homeServerName = SdkConfig.get()["validated_server_config"].hsName;
let subtitle;
switch (this.state.nature) {
case NATURE.DISAGREEMENT:
subtitle = _t("What this user is writing is wrong.\n" +
"This will be reported to the room moderators.");
break;
case NATURE.TOXIC:
subtitle = _t("This user is displaying toxic behaviour, " +
"for instance by insulting other users or sharing " +
" adult-only content in a family-friendly room " +
" or otherwise violating the rules of this room.\n" +
"This will be reported to the room moderators.");
break;
case NATURE.ILLEGAL:
subtitle = _t("This user is displaying illegal behaviour, " +
"for instance by doxing people or threatening violence.\n" +
"This will be reported to the room moderators who may escalate this to legal authorities.");
break;
case NATURE.SPAM:
subtitle = _t("This user is spamming the room with ads, links to ads or to propaganda.\n" +
"This will be reported to the room moderators.");
break;
case NON_STANDARD_NATURE.ADMIN:
if (client.isRoomEncrypted(this.props.mxEvent.getRoomId())) {
subtitle = _t("This room is dedicated to illegal or toxic content " +
"or the moderators fail to moderate illegal or toxic content.\n" +
"This will be reported to the administrators of %(homeserver)s. " +
"The administrators will NOT be able to read the encrypted content of this room.",
{ homeserver: homeServerName });
} else {
subtitle = _t("This room is dedicated to illegal or toxic content " +
"or the moderators fail to moderate illegal or toxic content.\n" +
" This will be reported to the administrators of %(homeserver)s.",
{ homeserver: homeServerName });
}
break;
case NATURE.OTHER:
subtitle = _t("Any other reason. Please describe the problem.\n" +
"This will be reported to the room moderators.");
break;
default:
subtitle = _t("Please pick a nature and describe what makes this message abusive.");
break;
}
return (
<BaseDialog
className="mx_ReportEventDialog"
onFinished={this.props.onFinished}
title={_t('Report Content')}
contentId='mx_ReportEventDialog'
>
<div>
<StyledRadioButton
name = "nature"
value = { NATURE.DISAGREEMENT }
checked = { this.state.nature == NATURE.DISAGREEMENT }
onChange = { this.onNatureChosen }
>
{_t('Disagree')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.TOXIC }
checked = { this.state.nature == NATURE.TOXIC }
onChange = { this.onNatureChosen }
>
{_t('Toxic Behaviour')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.ILLEGAL }
checked = { this.state.nature == NATURE.ILLEGAL }
onChange = { this.onNatureChosen }
>
{_t('Illegal Content')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.SPAM }
checked = { this.state.nature == NATURE.SPAM }
onChange = { this.onNatureChosen }
>
{_t('Spam or propaganda')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NON_STANDARD_NATURE.ADMIN }
checked = { this.state.nature == NON_STANDARD_NATURE.ADMIN }
onChange = { this.onNatureChosen }
>
{_t('Report the entire room')}
</StyledRadioButton>
<StyledRadioButton
name = "nature"
value = { NATURE.OTHER }
checked = { this.state.nature == NATURE.OTHER }
onChange = { this.onNatureChosen }
>
{_t('Other')}
</StyledRadioButton>
<p>
{subtitle}
</p>
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this.onSubmit}
focus={true}
onCancel={this.onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
// Report to homeserver admin.
// Currently, the API does not support natures.
return (
<BaseDialog
className="mx_ReportEventDialog"
onFinished={this.props.onFinished}
title={_t('Report Content to Your Homeserver Administrator')}
contentId='mx_ReportEventDialog'
>
<div className="mx_ReportEventDialog" id="mx_ReportEventDialog">
<p>
{
_t("Reporting this message will send its unique 'event ID' to the administrator of " +
"your homeserver. If messages in this room are encrypted, your homeserver " +
"administrator will not be able to read the message text or view any files " +
"or images.")
}
</p>
{adminMessage}
<Field
className="mx_ReportEventDialog_reason"
element="textarea"
label={_t("Reason")}
rows={5}
onChange={this.onReasonChange}
value={this.state.reason}
disabled={this.state.busy}
/>
{progress}
{error}
</div>
<DialogButtons
primaryButton={_t("Send report")}
onPrimaryButtonClick={this.onSubmit}
focus={true}
onCancel={this.onCancel}
disabled={this.state.busy}
/>
</BaseDialog>
);
}
}

View file

@ -108,7 +108,10 @@ export default class RoomSettingsDialog extends React.Component<IProps> {
ROOM_ADVANCED_TAB, ROOM_ADVANCED_TAB,
_td("Advanced"), _td("Advanced"),
"mx_RoomSettingsDialog_warningIcon", "mx_RoomSettingsDialog_warningIcon",
<AdvancedRoomSettingsTab roomId={this.props.roomId} closeSettingsFn={this.props.onFinished} />, <AdvancedRoomSettingsTab
roomId={this.props.roomId}
closeSettingsFn={() => this.props.onFinished(true)}
/>,
)); ));
} }

View file

@ -14,24 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import React, {useState} from 'react'; import React, { useMemo } from 'react';
import {Room} from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import {MatrixClient} from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import {EventType} from "matrix-js-sdk/src/@types/event";
import {_t} from '../../../languageHandler'; import { _t, _td } from '../../../languageHandler';
import {IDialogProps} from "./IDialogProps"; import { IDialogProps } from "./IDialogProps";
import BaseDialog from "./BaseDialog"; import BaseDialog from "./BaseDialog";
import DevtoolsDialog from "./DevtoolsDialog";
import SpaceBasicSettings from '../spaces/SpaceBasicSettings';
import {getTopic} from "../elements/RoomTopic";
import {avatarUrlForRoom} from "../../../Avatar";
import ToggleSwitch from "../elements/ToggleSwitch";
import AccessibleButton from "../elements/AccessibleButton";
import Modal from "../../../Modal";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import {useDispatcher} from "../../../hooks/useDispatcher"; import { useDispatcher } from "../../../hooks/useDispatcher";
import {SpaceFeedbackPrompt} from "../../structures/SpaceRoomView"; import TabbedView, { Tab } from "../../structures/TabbedView";
import SpaceSettingsGeneralTab from '../spaces/SpaceSettingsGeneralTab';
import SpaceSettingsVisibilityTab from "../spaces/SpaceSettingsVisibilityTab";
import SettingsStore from "../../../settings/SettingsStore";
import { UIFeature } from "../../../settings/UIFeature";
import AdvancedRoomSettingsTab from "../settings/tabs/room/AdvancedRoomSettingsTab";
export enum SpaceSettingsTab {
General = "SPACE_GENERAL_TAB",
Visibility = "SPACE_VISIBILITY_TAB",
Advanced = "SPACE_ADVANCED_TAB",
}
interface IProps extends IDialogProps { interface IProps extends IDialogProps {
matrixClient: MatrixClient; matrixClient: MatrixClient;
@ -45,63 +48,30 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
} }
}); });
const [busy, setBusy] = useState(false); const tabs = useMemo(() => {
const [error, setError] = useState(""); return [
new Tab(
const userId = cli.getUserId(); SpaceSettingsTab.General,
_td("General"),
const [newAvatar, setNewAvatar] = useState<File>(null); // undefined means to remove avatar "mx_SpaceSettingsDialog_generalIcon",
const canSetAvatar = space.currentState.maySendStateEvent(EventType.RoomAvatar, userId); <SpaceSettingsGeneralTab matrixClient={cli} space={space} onFinished={onFinished} />,
const avatarChanged = newAvatar !== null; ),
new Tab(
const [name, setName] = useState<string>(space.name); SpaceSettingsTab.Visibility,
const canSetName = space.currentState.maySendStateEvent(EventType.RoomName, userId); _td("Visibility"),
const nameChanged = name !== space.name; "mx_SpaceSettingsDialog_visibilityIcon",
<SpaceSettingsVisibilityTab matrixClient={cli} space={space} />,
const currentTopic = getTopic(space); ),
const [topic, setTopic] = useState<string>(currentTopic); SettingsStore.getValue(UIFeature.AdvancedSettings)
const canSetTopic = space.currentState.maySendStateEvent(EventType.RoomTopic, userId); ? new Tab(
const topicChanged = topic !== currentTopic; SpaceSettingsTab.Advanced,
_td("Advanced"),
const currentJoinRule = space.getJoinRule(); "mx_RoomSettingsDialog_warningIcon",
const [joinRule, setJoinRule] = useState(currentJoinRule); <AdvancedRoomSettingsTab roomId={space.roomId} closeSettingsFn={onFinished} />,
const canSetJoinRule = space.currentState.maySendStateEvent(EventType.RoomJoinRules, userId); )
const joinRuleChanged = joinRule !== currentJoinRule; : null,
].filter(Boolean);
const onSave = async () => { }, [cli, space, onFinished]);
setBusy(true);
const promises = [];
if (avatarChanged) {
if (newAvatar) {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {
url: await cli.uploadContent(newAvatar),
}, ""));
} else {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomAvatar, {}, ""));
}
}
if (nameChanged) {
promises.push(cli.setRoomName(space.roomId, name));
}
if (topicChanged) {
promises.push(cli.setRoomTopic(space.roomId, topic));
}
if (joinRuleChanged) {
promises.push(cli.sendStateEvent(space.roomId, EventType.RoomJoinRules, { join_rule: joinRule }, ""));
}
const results = await Promise.allSettled(promises);
setBusy(false);
const failures = results.filter(r => r.status === "rejected");
if (failures.length > 0) {
console.error("Failed to save space settings: ", failures);
setError(_t("Failed to save space settings."));
}
};
return <BaseDialog return <BaseDialog
title={_t("Space settings")} title={_t("Space settings")}
@ -110,61 +80,14 @@ const SpaceSettingsDialog: React.FC<IProps> = ({ matrixClient: cli, space, onFin
onFinished={onFinished} onFinished={onFinished}
fixedWidth={false} fixedWidth={false}
> >
<div className="mx_SpaceSettingsDialog_content" id="mx_SpaceSettingsDialog"> <div
<div>{ _t("Edit settings relating to your space.") }</div> className="mx_SpaceSettingsDialog_content"
id="mx_SpaceSettingsDialog"
{ error && <div className="mx_SpaceRoomView_errorText">{ error }</div> } title={_t("Settings - %(spaceName)s", { spaceName: space.name })}
>
<SpaceFeedbackPrompt onClick={() => onFinished(false)} /> <TabbedView tabs={tabs} />
<SpaceBasicSettings
avatarUrl={avatarUrlForRoom(space, 80, 80, "crop")}
avatarDisabled={busy || !canSetAvatar}
setAvatar={setNewAvatar}
name={name}
nameDisabled={busy || !canSetName}
setName={setName}
topic={topic}
topicDisabled={busy || !canSetTopic}
setTopic={setTopic}
/>
<div>
{ _t("Make this space private") }
<ToggleSwitch
checked={joinRule !== "public"}
onChange={checked => setJoinRule(checked ? "invite" : "public")}
disabled={!canSetJoinRule}
aria-label={_t("Make this space private")}
/>
</div>
<AccessibleButton
kind="danger"
onClick={() => {
defaultDispatcher.dispatch({
action: "leave_room",
room_id: space.roomId,
});
}}
>
{ _t("Leave Space") }
</AccessibleButton>
<div className="mx_SpaceSettingsDialog_buttons">
<AccessibleButton onClick={() => Modal.createDialog(DevtoolsDialog, {roomId: space.roomId})}>
{ _t("View dev tools") }
</AccessibleButton>
<AccessibleButton onClick={onFinished} disabled={busy} kind="link">
{ _t("Cancel") }
</AccessibleButton>
<AccessibleButton onClick={onSave} disabled={busy} kind="primary">
{ busy ? _t("Saving...") : _t("Save Changes") }
</AccessibleButton>
</div>
</div> </div>
</BaseDialog>; </BaseDialog>;
}; };
export default SpaceSettingsDialog; export default SpaceSettingsDialog;

View file

@ -16,22 +16,21 @@ limitations under the License.
import url from 'url'; import url from 'url';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import * as sdk from '../../../index'; import * as sdk from '../../../index';
import { _t, pickBestLanguage } from '../../../languageHandler'; import { _t, pickBestLanguage } from '../../../languageHandler';
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
import {SERVICE_TYPES} from "matrix-js-sdk/src/service-types"; import { SERVICE_TYPES } from "matrix-js-sdk/src/service-types";
class TermsCheckbox extends React.PureComponent { interface ITermsCheckboxProps {
static propTypes = { onChange: (url: string, checked: boolean) => void;
onChange: PropTypes.func.isRequired, url: string;
url: PropTypes.string.isRequired, checked: boolean;
checked: PropTypes.bool.isRequired, }
}
onChange = (ev) => { class TermsCheckbox extends React.PureComponent<ITermsCheckboxProps> {
this.props.onChange(this.props.url, ev.target.checked); private onChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.props.onChange(this.props.url, ev.currentTarget.checked);
} }
render() { render() {
@ -42,30 +41,34 @@ class TermsCheckbox extends React.PureComponent {
} }
} }
interface ITermsDialogProps {
/**
* Array of [Service, policies] pairs, where policies is the response from the
* /terms endpoint for that service
*/
policiesAndServicePairs: any[],
/**
* urls that the user has already agreed to
*/
agreedUrls?: string[],
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: (success: boolean, agreedUrls?: string[]) => void,
}
interface IState {
agreedUrls: any;
}
@replaceableComponent("views.dialogs.TermsDialog") @replaceableComponent("views.dialogs.TermsDialog")
export default class TermsDialog extends React.PureComponent { export default class TermsDialog extends React.PureComponent<ITermsDialogProps, IState> {
static propTypes = {
/**
* Array of [Service, policies] pairs, where policies is the response from the
* /terms endpoint for that service
*/
policiesAndServicePairs: PropTypes.array.isRequired,
/**
* urls that the user has already agreed to
*/
agreedUrls: PropTypes.arrayOf(PropTypes.string),
/**
* Called with:
* * success {bool} True if the user accepted any douments, false if cancelled
* * agreedUrls {string[]} List of agreed URLs
*/
onFinished: PropTypes.func.isRequired,
}
constructor(props) { constructor(props) {
super(); super(props);
this.state = { this.state = {
// url -> boolean // url -> boolean
agreedUrls: {}, agreedUrls: {},
@ -75,15 +78,15 @@ export default class TermsDialog extends React.PureComponent {
} }
} }
_onCancelClick = () => { private onCancelClick = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} }
_onNextClick = () => { private onNextClick = (): void => {
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url])); this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
} }
_nameForServiceType(serviceType, host) { private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
switch (serviceType) { switch (serviceType) {
case SERVICE_TYPES.IS: case SERVICE_TYPES.IS:
return <div>{_t("Identity Server")}<br />({host})</div>; return <div>{_t("Identity Server")}<br />({host})</div>;
@ -92,7 +95,7 @@ export default class TermsDialog extends React.PureComponent {
} }
} }
_summaryForServiceType(serviceType) { private summaryForServiceType(serviceType: SERVICE_TYPES): JSX.Element {
switch (serviceType) { switch (serviceType) {
case SERVICE_TYPES.IS: case SERVICE_TYPES.IS:
return <div> return <div>
@ -107,13 +110,13 @@ export default class TermsDialog extends React.PureComponent {
} }
} }
_onTermsCheckboxChange = (url, checked) => { private onTermsCheckboxChange = (url: string, checked: boolean) => {
this.setState({ this.setState({
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }), agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
}); });
} }
render() { public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
@ -128,8 +131,8 @@ export default class TermsDialog extends React.PureComponent {
let serviceName; let serviceName;
let summary; let summary;
if (i === 0) { if (i === 0) {
serviceName = this._nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host); serviceName = this.nameForServiceType(policiesAndService.service.serviceType, parsedBaseUrl.host);
summary = this._summaryForServiceType( summary = this.summaryForServiceType(
policiesAndService.service.serviceType, policiesAndService.service.serviceType,
); );
} }
@ -137,12 +140,15 @@ export default class TermsDialog extends React.PureComponent {
rows.push(<tr key={termDoc[termsLang].url}> rows.push(<tr key={termDoc[termsLang].url}>
<td className="mx_TermsDialog_service">{serviceName}</td> <td className="mx_TermsDialog_service">{serviceName}</td>
<td className="mx_TermsDialog_summary">{summary}</td> <td className="mx_TermsDialog_summary">{summary}</td>
<td>{termDoc[termsLang].name} <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}> <td>
<span className="mx_TermsDialog_link" /> {termDoc[termsLang].name}
</a></td> <a rel="noreferrer noopener" target="_blank" href={termDoc[termsLang].url}>
<span className="mx_TermsDialog_link" />
</a>
</td>
<td><TermsCheckbox <td><TermsCheckbox
url={termDoc[termsLang].url} url={termDoc[termsLang].url}
onChange={this._onTermsCheckboxChange} onChange={this.onTermsCheckboxChange}
checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])} checked={Boolean(this.state.agreedUrls[termDoc[termsLang].url])}
/></td> /></td>
</tr>); </tr>);
@ -176,7 +182,7 @@ export default class TermsDialog extends React.PureComponent {
return ( return (
<BaseDialog <BaseDialog
fixedWidth={false} fixedWidth={false}
onFinished={this._onCancelClick} onFinished={this.onCancelClick}
title={_t("Terms of Service")} title={_t("Terms of Service")}
contentId='mx_Dialog_content' contentId='mx_Dialog_content'
hasCancel={false} hasCancel={false}
@ -197,8 +203,8 @@ export default class TermsDialog extends React.PureComponent {
<DialogButtons primaryButton={_t('Next')} <DialogButtons primaryButton={_t('Next')}
hasCancel={true} hasCancel={true}
onCancel={this._onCancelClick} onCancel={this.onCancelClick}
onPrimaryButtonClick={this._onNextClick} onPrimaryButtonClick={this.onNextClick}
primaryDisabled={!enableSubmit} primaryDisabled={!enableSubmit}
/> />
</BaseDialog> </BaseDialog>

View file

@ -16,11 +16,10 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import TabbedView, {Tab} from "../../structures/TabbedView"; import TabbedView, {Tab} from "../../structures/TabbedView";
import {_t, _td} from "../../../languageHandler"; import {_t, _td} from "../../../languageHandler";
import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab"; import GeneralUserSettingsTab from "../settings/tabs/user/GeneralUserSettingsTab";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore, { CallbackFn } from "../../../settings/SettingsStore";
import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab"; import LabsUserSettingsTab from "../settings/tabs/user/LabsUserSettingsTab";
import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab"; import AppearanceUserSettingsTab from "../settings/tabs/user/AppearanceUserSettingsTab";
import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab"; import SecurityUserSettingsTab from "../settings/tabs/user/SecurityUserSettingsTab";
@ -35,41 +34,49 @@ import MjolnirUserSettingsTab from "../settings/tabs/user/MjolnirUserSettingsTab
import {UIFeature} from "../../../settings/UIFeature"; import {UIFeature} from "../../../settings/UIFeature";
import {replaceableComponent} from "../../../utils/replaceableComponent"; import {replaceableComponent} from "../../../utils/replaceableComponent";
export const USER_GENERAL_TAB = "USER_GENERAL_TAB"; export enum UserTab {
export const USER_APPEARANCE_TAB = "USER_APPEARANCE_TAB"; General = "USER_GENERAL_TAB",
export const USER_FLAIR_TAB = "USER_FLAIR_TAB"; Appearance = "USER_APPEARANCE_TAB",
export const USER_NOTIFICATIONS_TAB = "USER_NOTIFICATIONS_TAB"; Flair = "USER_FLAIR_TAB",
export const USER_PREFERENCES_TAB = "USER_PREFERENCES_TAB"; Notifications = "USER_NOTIFICATIONS_TAB",
export const USER_VOICE_TAB = "USER_VOICE_TAB"; Preferences = "USER_PREFERENCES_TAB",
export const USER_SECURITY_TAB = "USER_SECURITY_TAB"; Voice = "USER_VOICE_TAB",
export const USER_LABS_TAB = "USER_LABS_TAB"; Security = "USER_SECURITY_TAB",
export const USER_MJOLNIR_TAB = "USER_MJOLNIR_TAB"; Labs = "USER_LABS_TAB",
export const USER_HELP_TAB = "USER_HELP_TAB"; Mjolnir = "USER_MJOLNIR_TAB",
Help = "USER_HELP_TAB",
}
interface IProps {
onFinished: (success: boolean) => void;
initialTabId?: string;
}
interface IState {
mjolnirEnabled: boolean;
}
@replaceableComponent("views.dialogs.UserSettingsDialog") @replaceableComponent("views.dialogs.UserSettingsDialog")
export default class UserSettingsDialog extends React.Component { export default class UserSettingsDialog extends React.Component<IProps, IState> {
static propTypes = { private mjolnirWatcher: string;
onFinished: PropTypes.func.isRequired,
initialTabId: PropTypes.string,
};
constructor() { constructor(props) {
super(); super(props);
this.state = { this.state = {
mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"), mjolnirEnabled: SettingsStore.getValue("feature_mjolnir"),
}; };
} }
componentDidMount(): void { public componentDidMount(): void {
this._mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this._mjolnirChanged.bind(this)); this.mjolnirWatcher = SettingsStore.watchSetting("feature_mjolnir", null, this.mjolnirChanged);
} }
componentWillUnmount(): void { public componentWillUnmount(): void {
SettingsStore.unwatchSetting(this._mjolnirWatcher); SettingsStore.unwatchSetting(this.mjolnirWatcher);
} }
_mjolnirChanged(settingName, roomId, atLevel, newValue) { private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
// We can cheat because we know what levels a feature is tracked at, and how it is tracked // We can cheat because we know what levels a feature is tracked at, and how it is tracked
this.setState({mjolnirEnabled: newValue}); this.setState({mjolnirEnabled: newValue});
} }
@ -78,33 +85,33 @@ export default class UserSettingsDialog extends React.Component {
const tabs = []; const tabs = [];
tabs.push(new Tab( tabs.push(new Tab(
USER_GENERAL_TAB, UserTab.General,
_td("General"), _td("General"),
"mx_UserSettingsDialog_settingsIcon", "mx_UserSettingsDialog_settingsIcon",
<GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />, <GeneralUserSettingsTab closeSettingsFn={this.props.onFinished} />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_APPEARANCE_TAB, UserTab.Appearance,
_td("Appearance"), _td("Appearance"),
"mx_UserSettingsDialog_appearanceIcon", "mx_UserSettingsDialog_appearanceIcon",
<AppearanceUserSettingsTab />, <AppearanceUserSettingsTab />,
)); ));
if (SettingsStore.getValue(UIFeature.Flair)) { if (SettingsStore.getValue(UIFeature.Flair)) {
tabs.push(new Tab( tabs.push(new Tab(
USER_FLAIR_TAB, UserTab.Flair,
_td("Flair"), _td("Flair"),
"mx_UserSettingsDialog_flairIcon", "mx_UserSettingsDialog_flairIcon",
<FlairUserSettingsTab />, <FlairUserSettingsTab />,
)); ));
} }
tabs.push(new Tab( tabs.push(new Tab(
USER_NOTIFICATIONS_TAB, UserTab.Notifications,
_td("Notifications"), _td("Notifications"),
"mx_UserSettingsDialog_bellIcon", "mx_UserSettingsDialog_bellIcon",
<NotificationUserSettingsTab />, <NotificationUserSettingsTab />,
)); ));
tabs.push(new Tab( tabs.push(new Tab(
USER_PREFERENCES_TAB, UserTab.Preferences,
_td("Preferences"), _td("Preferences"),
"mx_UserSettingsDialog_preferencesIcon", "mx_UserSettingsDialog_preferencesIcon",
<PreferencesUserSettingsTab />, <PreferencesUserSettingsTab />,
@ -112,7 +119,7 @@ export default class UserSettingsDialog extends React.Component {
if (SettingsStore.getValue(UIFeature.Voip)) { if (SettingsStore.getValue(UIFeature.Voip)) {
tabs.push(new Tab( tabs.push(new Tab(
USER_VOICE_TAB, UserTab.Voice,
_td("Voice & Video"), _td("Voice & Video"),
"mx_UserSettingsDialog_voiceIcon", "mx_UserSettingsDialog_voiceIcon",
<VoiceUserSettingsTab />, <VoiceUserSettingsTab />,
@ -120,7 +127,7 @@ export default class UserSettingsDialog extends React.Component {
} }
tabs.push(new Tab( tabs.push(new Tab(
USER_SECURITY_TAB, UserTab.Security,
_td("Security & Privacy"), _td("Security & Privacy"),
"mx_UserSettingsDialog_securityIcon", "mx_UserSettingsDialog_securityIcon",
<SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />, <SecurityUserSettingsTab closeSettingsFn={this.props.onFinished} />,
@ -130,7 +137,7 @@ export default class UserSettingsDialog extends React.Component {
|| SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k)) || SettingsStore.getFeatureSettingNames().some(k => SettingsStore.getBetaInfo(k))
) { ) {
tabs.push(new Tab( tabs.push(new Tab(
USER_LABS_TAB, UserTab.Labs,
_td("Labs"), _td("Labs"),
"mx_UserSettingsDialog_labsIcon", "mx_UserSettingsDialog_labsIcon",
<LabsUserSettingsTab />, <LabsUserSettingsTab />,
@ -138,17 +145,17 @@ export default class UserSettingsDialog extends React.Component {
} }
if (this.state.mjolnirEnabled) { if (this.state.mjolnirEnabled) {
tabs.push(new Tab( tabs.push(new Tab(
USER_MJOLNIR_TAB, UserTab.Mjolnir,
_td("Ignored users"), _td("Ignored users"),
"mx_UserSettingsDialog_mjolnirIcon", "mx_UserSettingsDialog_mjolnirIcon",
<MjolnirUserSettingsTab />, <MjolnirUserSettingsTab />,
)); ));
} }
tabs.push(new Tab( tabs.push(new Tab(
USER_HELP_TAB, UserTab.Help,
_td("Help & About"), _td("Help & About"),
"mx_UserSettingsDialog_helpIcon", "mx_UserSettingsDialog_helpIcon",
<HelpUserSettingsTab closeSettingsFn={this.props.onFinished} />, <HelpUserSettingsTab closeSettingsFn={() => this.props.onFinished(true)} />,
)); ));
return tabs; return tabs;

View file

@ -15,22 +15,21 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { _t } from "../../../../languageHandler";
import {_t} from "../../../../languageHandler";
import * as sdk from "../../../../index"; import * as sdk from "../../../../index";
import {replaceableComponent} from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
interface IProps {
onFinished: (success: boolean) => void;
}
@replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog") @replaceableComponent("views.dialogs.security.ConfirmDestroyCrossSigningDialog")
export default class ConfirmDestroyCrossSigningDialog extends React.Component { export default class ConfirmDestroyCrossSigningDialog extends React.Component<IProps> {
static propTypes = { private onConfirm = (): void => {
onFinished: PropTypes.func.isRequired,
};
_onConfirm = () => {
this.props.onFinished(true); this.props.onFinished(true);
}; };
_onDecline = () => { private onDecline = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
}; };
@ -57,10 +56,10 @@ export default class ConfirmDestroyCrossSigningDialog extends React.Component {
</div> </div>
<DialogButtons <DialogButtons
primaryButton={_t("Clear cross-signing keys")} primaryButton={_t("Clear cross-signing keys")}
onPrimaryButtonClick={this._onConfirm} onPrimaryButtonClick={this.onConfirm}
primaryButtonClass="danger" primaryButtonClass="danger"
cancelButton={_t("Cancel")} cancelButton={_t("Cancel")}
onCancel={this._onDecline} onCancel={this.onDecline}
/> />
</BaseDialog> </BaseDialog>
); );

View file

@ -16,7 +16,6 @@ limitations under the License.
*/ */
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { MatrixClientPeg } from '../../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler'; import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal'; import Modal from '../../../../Modal';
@ -25,7 +24,19 @@ import DialogButtons from '../../elements/DialogButtons';
import BaseDialog from '../BaseDialog'; import BaseDialog from '../BaseDialog';
import Spinner from '../../elements/Spinner'; import Spinner from '../../elements/Spinner';
import InteractiveAuthDialog from '../InteractiveAuthDialog'; import InteractiveAuthDialog from '../InteractiveAuthDialog';
import {replaceableComponent} from "../../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../../utils/replaceableComponent";
interface IProps {
accountPassword?: string;
tokenLogin?: boolean;
onFinished?: (success: boolean) => void;
}
interface IState {
error: Error | null;
canUploadKeysWithPasswordOnly?: boolean;
accountPassword: string;
}
/* /*
* Walks the user through the process of creating a cross-signing keys. In most * Walks the user through the process of creating a cross-signing keys. In most
@ -33,39 +44,32 @@ import {replaceableComponent} from "../../../../utils/replaceableComponent";
* may need to complete some steps to proceed. * may need to complete some steps to proceed.
*/ */
@replaceableComponent("views.dialogs.security.CreateCrossSigningDialog") @replaceableComponent("views.dialogs.security.CreateCrossSigningDialog")
export default class CreateCrossSigningDialog extends React.PureComponent { export default class CreateCrossSigningDialog extends React.PureComponent<IProps, IState> {
static propTypes = { constructor(props: IProps) {
accountPassword: PropTypes.string,
tokenLogin: PropTypes.bool,
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
error: null, error: null,
// Does the server offer a UI auth flow with just m.login.password // Does the server offer a UI auth flow with just m.login.password
// for /keys/device_signing/upload? // for /keys/device_signing/upload?
canUploadKeysWithPasswordOnly: null,
accountPassword: props.accountPassword || "",
};
if (this.state.accountPassword) {
// If we have an account password in memory, let's simplify and // If we have an account password in memory, let's simplify and
// assume it means password auth is also supported for device // assume it means password auth is also supported for device
// signing key upload as well. This avoids hitting the server to // signing key upload as well. This avoids hitting the server to
// test auth flows, which may be slow under high load. // test auth flows, which may be slow under high load.
this.state.canUploadKeysWithPasswordOnly = true; canUploadKeysWithPasswordOnly: props.accountPassword ? true : null,
} else { accountPassword: props.accountPassword || "",
this._queryKeyUploadAuth(); };
if (!this.state.accountPassword) {
this.queryKeyUploadAuth();
} }
} }
componentDidMount() { public componentDidMount(): void {
this._bootstrapCrossSigning(); this.bootstrapCrossSigning();
} }
async _queryKeyUploadAuth() { private async queryKeyUploadAuth(): Promise<void> {
try { try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {}); await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
// We should never get here: the server should always require // We should never get here: the server should always require
@ -86,7 +90,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
} }
} }
_doBootstrapUIAuth = async (makeRequest) => { private doBootstrapUIAuth = async (makeRequest: (authData: any) => void): Promise<void> => {
if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) { if (this.state.canUploadKeysWithPasswordOnly && this.state.accountPassword) {
await makeRequest({ await makeRequest({
type: 'm.login.password', type: 'm.login.password',
@ -137,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
} }
} }
_bootstrapCrossSigning = async () => { private bootstrapCrossSigning = async (): Promise<void> => {
this.setState({ this.setState({
error: null, error: null,
}); });
@ -146,13 +150,13 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
try { try {
await cli.bootstrapCrossSigning({ await cli.bootstrapCrossSigning({
authUploadDeviceSigningKeys: this._doBootstrapUIAuth, authUploadDeviceSigningKeys: this.doBootstrapUIAuth,
}); });
this.props.onFinished(true); this.props.onFinished(true);
} catch (e) { } catch (e) {
if (this.props.tokenLogin) { if (this.props.tokenLogin) {
// ignore any failures, we are relying on grace period here // ignore any failures, we are relying on grace period here
this.props.onFinished(); this.props.onFinished(false);
return; return;
} }
@ -161,7 +165,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
} }
} }
_onCancel = () => { private onCancel = (): void => {
this.props.onFinished(false); this.props.onFinished(false);
} }
@ -172,8 +176,8 @@ export default class CreateCrossSigningDialog extends React.PureComponent {
<p>{_t("Unable to set up keys")}</p> <p>{_t("Unable to set up keys")}</p>
<div className="mx_Dialog_buttons"> <div className="mx_Dialog_buttons">
<DialogButtons primaryButton={_t('Retry')} <DialogButtons primaryButton={_t('Retry')}
onPrimaryButtonClick={this._bootstrapCrossSigning} onPrimaryButtonClick={this.bootstrapCrossSigning}
onCancel={this._onCancel} onCancel={this.onCancel}
/> />
</div> </div>
</div>; </div>;

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