Merge branch 'experimental' into bwindels/smarterresizer
This commit is contained in:
commit
1bbf1502ec
62 changed files with 1703 additions and 420 deletions
|
@ -1,8 +1,5 @@
|
||||||
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
# autogenerated file: run scripts/generate-eslint-error-ignore-file to update.
|
||||||
|
|
||||||
src/autocomplete/AutocompleteProvider.js
|
|
||||||
src/autocomplete/Autocompleter.js
|
|
||||||
src/autocomplete/UserProvider.js
|
|
||||||
src/component-index.js
|
src/component-index.js
|
||||||
src/components/structures/BottomLeftMenu.js
|
src/components/structures/BottomLeftMenu.js
|
||||||
src/components/structures/CompatibilityPage.js
|
src/components/structures/CompatibilityPage.js
|
||||||
|
@ -18,7 +15,6 @@ src/components/structures/ScrollPanel.js
|
||||||
src/components/structures/SearchBox.js
|
src/components/structures/SearchBox.js
|
||||||
src/components/structures/TimelinePanel.js
|
src/components/structures/TimelinePanel.js
|
||||||
src/components/structures/UploadBar.js
|
src/components/structures/UploadBar.js
|
||||||
src/components/structures/UserSettings.js
|
|
||||||
src/components/views/avatars/BaseAvatar.js
|
src/components/views/avatars/BaseAvatar.js
|
||||||
src/components/views/avatars/MemberAvatar.js
|
src/components/views/avatars/MemberAvatar.js
|
||||||
src/components/views/create_room/RoomAlias.js
|
src/components/views/create_room/RoomAlias.js
|
||||||
|
@ -54,7 +50,6 @@ src/components/views/rooms/MemberInfo.js
|
||||||
src/components/views/rooms/MemberList.js
|
src/components/views/rooms/MemberList.js
|
||||||
src/components/views/rooms/MemberTile.js
|
src/components/views/rooms/MemberTile.js
|
||||||
src/components/views/rooms/MessageComposer.js
|
src/components/views/rooms/MessageComposer.js
|
||||||
src/components/views/rooms/MessageComposerInput.js
|
|
||||||
src/components/views/rooms/PinnedEventTile.js
|
src/components/views/rooms/PinnedEventTile.js
|
||||||
src/components/views/rooms/RoomList.js
|
src/components/views/rooms/RoomList.js
|
||||||
src/components/views/rooms/RoomPreviewBar.js
|
src/components/views/rooms/RoomPreviewBar.js
|
||||||
|
|
20
.travis.yml
20
.travis.yml
|
@ -14,8 +14,22 @@ node_js:
|
||||||
addons:
|
addons:
|
||||||
chrome: stable
|
chrome: stable
|
||||||
install:
|
install:
|
||||||
- npm install
|
- ./scripts/travis/install-deps.sh
|
||||||
# install synapse prerequisites for end to end tests
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Linting Checks
|
||||||
|
script:
|
||||||
|
# run the linter, but exclude any files known to have errors or warnings.
|
||||||
|
- npm run lintwithexclusions
|
||||||
|
- name: End-to-End Tests
|
||||||
|
if: branch = develop
|
||||||
|
install:
|
||||||
- sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev
|
- sudo apt-get install build-essential python2.7-dev libffi-dev python-pip python-setuptools sqlite3 libssl-dev python-virtualenv libjpeg-dev libxslt1-dev
|
||||||
script:
|
script:
|
||||||
./scripts/travis.sh
|
- ./scripts/travis/end-to-end-tests.sh
|
||||||
|
- name: Unit Tests
|
||||||
|
script:
|
||||||
|
- ./scripts/travis/unit-tests.sh
|
||||||
|
- name: Riot-web Unit Tests
|
||||||
|
script:
|
||||||
|
- ./scripts/travis/riot-unit-tests.sh
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"start:init": "babel src -d lib --source-maps --copy-files",
|
"start:init": "babel src -d lib --source-maps --copy-files",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
"lintall": "eslint src/ test/",
|
"lintall": "eslint src/ test/",
|
||||||
"lintwithexclusions": "eslint --max-warnings 16 --ignore-path .eslintignore.errorfiles src test",
|
"lintwithexclusions": "eslint --max-warnings 18 --ignore-path .eslintignore.errorfiles src test",
|
||||||
"clean": "rimraf lib",
|
"clean": "rimraf lib",
|
||||||
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
|
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
|
||||||
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
"test": "karma start --single-run=true --browsers ChromeHeadless",
|
||||||
|
@ -118,7 +118,7 @@
|
||||||
"babel-preset-react": "^6.24.1",
|
"babel-preset-react": "^6.24.1",
|
||||||
"chokidar": "^1.6.1",
|
"chokidar": "^1.6.1",
|
||||||
"concurrently": "^4.0.1",
|
"concurrently": "^4.0.1",
|
||||||
"eslint": "^5.8.0",
|
"eslint": "^5.12.0",
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.7.1",
|
||||||
"eslint-plugin-babel": "^5.2.1",
|
"eslint-plugin-babel": "^5.2.1",
|
||||||
"eslint-plugin-flowtype": "^2.30.0",
|
"eslint-plugin-flowtype": "^2.30.0",
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
@import "./structures/_ContextualMenu.scss";
|
@import "./structures/_ContextualMenu.scss";
|
||||||
@import "./structures/_CreateRoom.scss";
|
@import "./structures/_CreateRoom.scss";
|
||||||
@import "./structures/_FilePanel.scss";
|
@import "./structures/_FilePanel.scss";
|
||||||
|
@import "./structures/_GroupGridView.scss";
|
||||||
@import "./structures/_GroupView.scss";
|
@import "./structures/_GroupView.scss";
|
||||||
@import "./structures/_HomePage.scss";
|
@import "./structures/_HomePage.scss";
|
||||||
@import "./structures/_LeftPanel.scss";
|
@import "./structures/_LeftPanel.scss";
|
||||||
|
@ -50,7 +51,7 @@
|
||||||
@import "./views/dialogs/_ShareDialog.scss";
|
@import "./views/dialogs/_ShareDialog.scss";
|
||||||
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
@import "./views/dialogs/_UnknownDeviceDialog.scss";
|
||||||
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
@import "./views/dialogs/keybackup/_CreateKeyBackupDialog.scss";
|
||||||
@import "./views/dialogs/keybackup/_NewRecoveryMethodDialog.scss";
|
@import "./views/dialogs/keybackup/_KeyBackupFailedDialog.scss";
|
||||||
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
|
@import "./views/dialogs/keybackup/_RestoreKeyBackupDialog.scss";
|
||||||
@import "./views/directory/_NetworkDropdown.scss";
|
@import "./views/directory/_NetworkDropdown.scss";
|
||||||
@import "./views/elements/_AccessibleButton.scss";
|
@import "./views/elements/_AccessibleButton.scss";
|
||||||
|
@ -91,6 +92,7 @@
|
||||||
@import "./views/messages/_UnknownBody.scss";
|
@import "./views/messages/_UnknownBody.scss";
|
||||||
@import "./views/rooms/_AppsDrawer.scss";
|
@import "./views/rooms/_AppsDrawer.scss";
|
||||||
@import "./views/rooms/_Autocomplete.scss";
|
@import "./views/rooms/_Autocomplete.scss";
|
||||||
|
@import "./views/rooms/_AuxPanel.scss";
|
||||||
@import "./views/rooms/_EntityTile.scss";
|
@import "./views/rooms/_EntityTile.scss";
|
||||||
@import "./views/rooms/_EventTile.scss";
|
@import "./views/rooms/_EventTile.scss";
|
||||||
@import "./views/rooms/_LinkPreviewWidget.scss";
|
@import "./views/rooms/_LinkPreviewWidget.scss";
|
||||||
|
|
130
res/css/structures/_GroupGridView.scss
Normal file
130
res/css/structures/_GroupGridView.scss
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.mx_GroupGridView {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, calc(100% / 3));
|
||||||
|
grid-template-rows: repeat(2, calc(100% / 2));
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rightPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.mx_GroupGridView_tabs {
|
||||||
|
flex: 0 0 52px;
|
||||||
|
border-bottom: 1px solid $primary-hairline-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
justify-content: flex-end;
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_RightPanel {
|
||||||
|
flex: 1 0 auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.mx_GroupGridView > .mx_MainSplit {
|
||||||
|
flex: 1 1 0;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_emptyTile {
|
||||||
|
display: block;
|
||||||
|
margin-top: 100px;
|
||||||
|
text-align: center;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_tile {
|
||||||
|
border-right: 1px solid $panel-divider-color;
|
||||||
|
border-bottom: 1px solid $panel-divider-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_activeTile {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_activeTile:before,
|
||||||
|
.mx_GroupGridView_activeTile:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
content: "";
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 3500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_activeTile:before {
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 8px solid $gridview-focus-border-glow-color;
|
||||||
|
margin: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_activeTile:after {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 2px solid $gridview-focus-border-color;
|
||||||
|
margin: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_tile > .mx_RoomView {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms > *:nth-child(1) {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms > *:nth-child(2) {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms > *:nth-child(3) {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms > *:nth-child(4) {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms > *:nth-child(5) {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_GroupGridView_rooms > *:nth-child(6) {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 2;
|
||||||
|
}
|
|
@ -73,14 +73,16 @@ limitations under the License.
|
||||||
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) {
|
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) {
|
||||||
background-color: $primary-bg-color;
|
background-color: $primary-bg-color;
|
||||||
|
|
||||||
flex: 1;
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
/* Experimental fix for https://github.com/vector-im/vector-web/issues/947
|
/* Experimental fix for https://github.com/vector-im/vector-web/issues/947
|
||||||
and https://github.com/vector-im/vector-web/issues/946.
|
and https://github.com/vector-im/vector-web/issues/946.
|
||||||
Empirically this stops the MessagePanel's width exploding outwards when
|
Empirically this stops the MessagePanel's width exploding outwards when
|
||||||
gemini is in 'prevented' mode
|
gemini is in 'prevented' mode
|
||||||
*/
|
*/
|
||||||
overflow-x: auto;
|
// disabling this for now as it clips the active room rect on the grid view
|
||||||
|
// overflow-x: auto;
|
||||||
|
|
||||||
/* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari
|
/* To fix https://github.com/vector-im/riot-web/issues/3298 where Safari
|
||||||
needed height 100% all the way down to the HomePage. Height does not
|
needed height 100% all the way down to the HomePage. Height does not
|
||||||
|
|
|
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_NewRecoveryMethodDialog .mx_Dialog_title {
|
.mx_KeyBackupFailedDialog .mx_Dialog_title {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NewRecoveryMethodDialog_title {
|
.mx_KeyBackupFailedDialog_title {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-left: 45px;
|
padding-left: 45px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
|
|
||||||
&:before {
|
&:before {
|
||||||
mask: url("../../../img/e2e/lock-warning.svg");
|
mask: url("../../img/e2e/lock-warning.svg");
|
||||||
mask-repeat: no-repeat;
|
mask-repeat: no-repeat;
|
||||||
background-color: $primary-fg-color;
|
background-color: $primary-fg-color;
|
||||||
content: "";
|
content: "";
|
||||||
|
@ -36,6 +36,6 @@ limitations under the License.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_NewRecoveryMethodDialog .mx_Dialog_buttons {
|
.mx_KeyBackupFailedDialog .mx_Dialog_buttons {
|
||||||
margin-top: 36px;
|
margin-top: 36px;
|
||||||
}
|
}
|
50
res/css/views/rooms/_AuxPanel.scss
Normal file
50
res/css/views/rooms/_AuxPanel.scss
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews {
|
||||||
|
padding: 5px;
|
||||||
|
padding-left: 19px;
|
||||||
|
border-bottom: 1px solid #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews_span a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews_span[data-severity=warning] {
|
||||||
|
font-weight: bold;
|
||||||
|
color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews_span[data-severity=alert] {
|
||||||
|
font-weight: bold;
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews_span[data-severity=normal] {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews_span[data-severity=notice] {
|
||||||
|
font-weight: normal;
|
||||||
|
color: $settings-grey-fg-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m_RoomView_auxPanel_stateViews_delim {
|
||||||
|
padding: 0 5px;
|
||||||
|
color: $settings-grey-fg-color;
|
||||||
|
}
|
|
@ -53,6 +53,10 @@ limitations under the License.
|
||||||
.mx_MemberList_query,
|
.mx_MemberList_query,
|
||||||
.mx_GroupMemberList_query,
|
.mx_GroupMemberList_query,
|
||||||
.mx_GroupRoomList_query {
|
.mx_GroupRoomList_query {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx_MemberList .gm-scrollbar-container {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
res/img/feather-icons/toggle-right-panel.svg
Normal file
17
res/img/feather-icons/toggle-right-panel.svg
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg width="22px" height="14px" viewBox="0 0 22 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<!-- Generator: Sketch 52.5 (67469) - http://www.bohemiancoding.com/sketch -->
|
||||||
|
<title>Group 2</title>
|
||||||
|
<desc>Created with Sketch.</desc>
|
||||||
|
<g id="Experiments" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round">
|
||||||
|
<g id="multi-room-test-copy-8" transform="translate(-826.000000, -15.000000)" stroke="#929EB4" stroke-width="1.6">
|
||||||
|
<g id="Group-4" transform="translate(341.000000, 7.000000)">
|
||||||
|
<g id="Group-2" transform="translate(486.000000, 8.000000)">
|
||||||
|
<path d="M20,1 L2.30926389e-14,1" id="Line-10"></path>
|
||||||
|
<path d="M20,7 L3,7" id="Line-10-Copy"></path>
|
||||||
|
<path d="M20,13 L6,13" id="Line-10-Copy-2"></path>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 995 B |
|
@ -162,6 +162,10 @@ $lightbox-bg-color: #454545;
|
||||||
$lightbox-fg-color: #ffffff;
|
$lightbox-fg-color: #ffffff;
|
||||||
$lightbox-border-color: #ffffff;
|
$lightbox-border-color: #ffffff;
|
||||||
|
|
||||||
|
/*** GroupGridView ***/
|
||||||
|
$gridview-focus-border-glow-color: rgba(134, 193, 165, 0.5);
|
||||||
|
$gridview-focus-border-color: rgba(134, 193, 165, 1);
|
||||||
|
|
||||||
$imagebody-giflabel: rgba(1, 1, 1, 0.7);
|
$imagebody-giflabel: rgba(1, 1, 1, 0.7);
|
||||||
$imagebody-giflabel-border: rgba(1, 1, 1, 0.2);
|
$imagebody-giflabel-border: rgba(1, 1, 1, 0.2);
|
||||||
$imagebody-giflabel-color: rgba(0, 0, 0, 1);
|
$imagebody-giflabel-color: rgba(0, 0, 0, 1);
|
||||||
|
|
|
@ -184,6 +184,9 @@ $lightbox-bg-color: #454545;
|
||||||
$lightbox-fg-color: #ffffff;
|
$lightbox-fg-color: #ffffff;
|
||||||
$lightbox-border-color: #ffffff;
|
$lightbox-border-color: #ffffff;
|
||||||
|
|
||||||
|
/*** GroupGridView ***/
|
||||||
|
$gridview-focus-border-glow-color: rgba(134, 193, 165, 0.5);
|
||||||
|
$gridview-focus-border-color: rgba(134, 193, 165, 1);
|
||||||
// unused?
|
// unused?
|
||||||
$progressbar-color: #000;
|
$progressbar-color: #000;
|
||||||
|
|
||||||
|
|
|
@ -175,6 +175,10 @@ $lightbox-bg-color: #454545;
|
||||||
$lightbox-fg-color: #ffffff;
|
$lightbox-fg-color: #ffffff;
|
||||||
$lightbox-border-color: #ffffff;
|
$lightbox-border-color: #ffffff;
|
||||||
|
|
||||||
|
/*** GroupGridView ***/
|
||||||
|
$gridview-focus-border-glow-color: rgba(134, 193, 165, 0.5);
|
||||||
|
$gridview-focus-border-color: rgba(134, 193, 165, 1);
|
||||||
|
|
||||||
$imagebody-giflabel: rgba(0, 0, 0, 0.7);
|
$imagebody-giflabel: rgba(0, 0, 0, 0.7);
|
||||||
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
|
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
|
||||||
$imagebody-giflabel-color: rgba(255, 255, 255, 1);
|
$imagebody-giflabel-color: rgba(255, 255, 255, 1);
|
||||||
|
|
|
@ -24,18 +24,4 @@ rm -r node_modules/matrix-react-sdk
|
||||||
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
|
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
|
||||||
|
|
||||||
npm run build
|
npm run build
|
||||||
npm run test
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
if [ "$TRAVIS_BRANCH" = "develop" ]
|
|
||||||
then
|
|
||||||
# run end to end tests
|
|
||||||
scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master
|
|
||||||
pushd matrix-react-end-to-end-tests
|
|
||||||
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
|
|
||||||
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
|
||||||
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
|
||||||
./install.sh
|
|
||||||
./run.sh --travis
|
|
||||||
popd
|
|
||||||
fi
|
|
21
scripts/travis/end-to-end-tests.sh
Executable file
21
scripts/travis/end-to-end-tests.sh
Executable file
|
@ -0,0 +1,21 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# script which is run by the travis build (after `npm run test`).
|
||||||
|
#
|
||||||
|
# clones riot-web develop and runs the tests against our version of react-sdk.
|
||||||
|
|
||||||
|
set -ev
|
||||||
|
|
||||||
|
RIOT_WEB_DIR=riot-web
|
||||||
|
REACT_SDK_DIR=`pwd`
|
||||||
|
|
||||||
|
scripts/travis/build.sh
|
||||||
|
# run end to end tests
|
||||||
|
scripts/fetchdep.sh matrix-org matrix-react-end-to-end-tests master
|
||||||
|
pushd matrix-react-end-to-end-tests
|
||||||
|
ln -s $REACT_SDK_DIR/$RIOT_WEB_DIR riot/riot-web
|
||||||
|
# PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true ./install.sh
|
||||||
|
# CHROME_PATH=$(which google-chrome-stable) ./run.sh
|
||||||
|
./install.sh
|
||||||
|
./run.sh --travis
|
||||||
|
popd
|
|
@ -1,7 +1,7 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
|
npm install
|
||||||
scripts/fetchdep.sh matrix-org matrix-js-sdk
|
scripts/fetchdep.sh matrix-org matrix-js-sdk
|
||||||
rm -r node_modules/matrix-js-sdk || true
|
rm -r node_modules/matrix-js-sdk || true
|
||||||
ln -s ../matrix-js-sdk node_modules/matrix-js-sdk
|
ln -s ../matrix-js-sdk node_modules/matrix-js-sdk
|
||||||
|
@ -9,9 +9,3 @@ ln -s ../matrix-js-sdk node_modules/matrix-js-sdk
|
||||||
cd matrix-js-sdk
|
cd matrix-js-sdk
|
||||||
npm install
|
npm install
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
npm run test
|
|
||||||
./.travis-test-riot.sh
|
|
||||||
|
|
||||||
# run the linter, but exclude any files known to have errors or warnings.
|
|
||||||
npm run lintwithexclusions
|
|
14
scripts/travis/riot-unit-tests.sh
Executable file
14
scripts/travis/riot-unit-tests.sh
Executable file
|
@ -0,0 +1,14 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# script which is run by the travis build (after `npm run test`).
|
||||||
|
#
|
||||||
|
# clones riot-web develop and runs the tests against our version of react-sdk.
|
||||||
|
|
||||||
|
set -ev
|
||||||
|
|
||||||
|
RIOT_WEB_DIR=riot-web
|
||||||
|
|
||||||
|
scripts/travis/build.sh
|
||||||
|
pushd "$RIOT_WEB_DIR"
|
||||||
|
npm run test
|
||||||
|
popd
|
10
scripts/travis/unit-tests.sh
Executable file
10
scripts/travis/unit-tests.sh
Executable file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# script which is run by the travis build (after `npm run test`).
|
||||||
|
#
|
||||||
|
# clones riot-web develop and runs the tests against our version of react-sdk.
|
||||||
|
|
||||||
|
set -ev
|
||||||
|
|
||||||
|
scripts/travis/build.sh
|
||||||
|
npm run test
|
|
@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import RoomViewStore from './stores/RoomViewStore';
|
import OpenRoomsStore from './stores/OpenRoomsStore';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Consumes changes from the RoomViewStore and notifies specific things
|
* Consumes changes from the OpenRoomsStore and notifies specific things
|
||||||
* about when the active room changes. Unlike listening for RoomViewStore
|
* about when the active room changes. Unlike listening for RoomViewStore
|
||||||
* changes, you can subscribe to only changes relevant to a particular
|
* changes, you can subscribe to only changes relevant to a particular
|
||||||
* room.
|
* room.
|
||||||
|
@ -28,11 +28,15 @@ import RoomViewStore from './stores/RoomViewStore';
|
||||||
class ActiveRoomObserver {
|
class ActiveRoomObserver {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._listeners = {};
|
this._listeners = {};
|
||||||
|
const roomStore = OpenRoomsStore.getActiveRoomStore();
|
||||||
this._activeRoomId = RoomViewStore.getRoomId();
|
this._activeRoomId = roomStore && roomStore.getRoomId();
|
||||||
// TODO: We could self-destruct when the last listener goes away, or at least
|
// TODO: We could self-destruct when the last listener goes away, or at least
|
||||||
// stop listening.
|
// stop listening.
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate.bind(this));
|
this._roomStoreToken = OpenRoomsStore.addListener(this._onOpenRoomsStoreUpdate.bind(this));
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveRoomId() {
|
||||||
|
return this._activeRoomId;
|
||||||
}
|
}
|
||||||
|
|
||||||
addListener(roomId, listener) {
|
addListener(roomId, listener) {
|
||||||
|
@ -51,23 +55,23 @@ class ActiveRoomObserver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_emit(roomId) {
|
_emit(roomId, newActiveRoomId) {
|
||||||
if (!this._listeners[roomId]) return;
|
if (!this._listeners[roomId]) return;
|
||||||
|
|
||||||
for (const l of this._listeners[roomId]) {
|
for (const l of this._listeners[roomId]) {
|
||||||
l.call();
|
l.call(l, newActiveRoomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomViewStoreUpdate() {
|
_onOpenRoomsStoreUpdate() {
|
||||||
|
const activeRoomStore = OpenRoomsStore.getActiveRoomStore();
|
||||||
|
const newActiveRoomId = activeRoomStore && activeRoomStore.getRoomId();
|
||||||
// emit for the old room ID
|
// emit for the old room ID
|
||||||
if (this._activeRoomId) this._emit(this._activeRoomId);
|
if (this._activeRoomId) this._emit(this._activeRoomId, newActiveRoomId);
|
||||||
|
|
||||||
// update our cache
|
// update our cache
|
||||||
this._activeRoomId = RoomViewStore.getRoomId();
|
this._activeRoomId = newActiveRoomId;
|
||||||
|
|
||||||
// and emit for the new one
|
// and emit for the new one
|
||||||
if (this._activeRoomId) this._emit(this._activeRoomId);
|
if (this._activeRoomId) this._emit(this._activeRoomId, this._activeRoomId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ limitations under the License.
|
||||||
export default {
|
export default {
|
||||||
HomePage: "home_page",
|
HomePage: "home_page",
|
||||||
RoomView: "room_view",
|
RoomView: "room_view",
|
||||||
|
GroupGridView: "group_grid_view",
|
||||||
UserSettings: "user_settings",
|
UserSettings: "user_settings",
|
||||||
RoomDirectory: "room_directory",
|
RoomDirectory: "room_directory",
|
||||||
UserView: "user_view",
|
UserView: "user_view",
|
||||||
|
|
|
@ -129,6 +129,11 @@ function textForRoomNameEvent(ev) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textForTombstoneEvent(ev) {
|
||||||
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
|
return _t('%(senderDisplayName)s upgraded this room.', {senderDisplayName});
|
||||||
|
}
|
||||||
|
|
||||||
function textForServerACLEvent(ev) {
|
function textForServerACLEvent(ev) {
|
||||||
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
const senderDisplayName = ev.sender && ev.sender.name ? ev.sender.name : ev.getSender();
|
||||||
const prevContent = ev.getPrevContent();
|
const prevContent = ev.getPrevContent();
|
||||||
|
@ -433,6 +438,7 @@ const stateHandlers = {
|
||||||
'm.room.power_levels': textForPowerEvent,
|
'm.room.power_levels': textForPowerEvent,
|
||||||
'm.room.pinned_events': textForPinnedEvent,
|
'm.room.pinned_events': textForPinnedEvent,
|
||||||
'm.room.server_acl': textForServerACLEvent,
|
'm.room.server_acl': textForServerACLEvent,
|
||||||
|
'm.room.tombstone': textForTombstoneEvent,
|
||||||
|
|
||||||
'im.vector.modular.widgets': textForWidgetEvent,
|
'im.vector.modular.widgets': textForWidgetEvent,
|
||||||
};
|
};
|
||||||
|
|
|
@ -44,6 +44,7 @@ class UserActivity {
|
||||||
* Can be called multiple times with the same already running timer, which is a NO-OP.
|
* Can be called multiple times with the same already running timer, which is a NO-OP.
|
||||||
* Can be called before the user becomes active, in which case it is only started
|
* Can be called before the user becomes active, in which case it is only started
|
||||||
* later on when the user does become active.
|
* later on when the user does become active.
|
||||||
|
* @param {Timer} timer the timer to use
|
||||||
*/
|
*/
|
||||||
timeWhileActive(timer) {
|
timeWhileActive(timer) {
|
||||||
// important this happens first
|
// important this happens first
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
|
|
||||||
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.
|
||||||
|
@ -21,7 +21,7 @@ import { scorePassword } from '../../../../utils/PasswordScorer';
|
||||||
|
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
|
|
||||||
import { _t, _td } from '../../../../languageHandler';
|
import { _t } from '../../../../languageHandler';
|
||||||
|
|
||||||
const PHASE_PASSPHRASE = 0;
|
const PHASE_PASSPHRASE = 0;
|
||||||
const PHASE_PASSPHRASE_CONFIRM = 1;
|
const PHASE_PASSPHRASE_CONFIRM = 1;
|
||||||
|
@ -32,6 +32,7 @@ const PHASE_DONE = 5;
|
||||||
const PHASE_OPTOUT_CONFIRM = 6;
|
const PHASE_OPTOUT_CONFIRM = 6;
|
||||||
|
|
||||||
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
const PASSWORD_MIN_SCORE = 4; // So secure, many characters, much complex, wow, etc, etc.
|
||||||
|
const PASSPHRASE_FEEDBACK_DELAY = 500; // How long after keystroke to offer passphrase feedback, ms.
|
||||||
|
|
||||||
// XXX: copied from ShareDialog: factor out into utils
|
// XXX: copied from ShareDialog: factor out into utils
|
||||||
function selectText(target) {
|
function selectText(target) {
|
||||||
|
@ -63,6 +64,13 @@ export default React.createClass({
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this._recoveryKeyNode = null;
|
this._recoveryKeyNode = null;
|
||||||
this._keyBackupInfo = null;
|
this._keyBackupInfo = null;
|
||||||
|
this._setZxcvbnResultTimeout = null;
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_collectRecoveryKeyNode: function(n) {
|
_collectRecoveryKeyNode: function(n) {
|
||||||
|
@ -102,7 +110,7 @@ export default React.createClass({
|
||||||
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
info = await MatrixClientPeg.get().createKeyBackupVersion(
|
||||||
this._keyBackupInfo,
|
this._keyBackupInfo,
|
||||||
);
|
);
|
||||||
await MatrixClientPeg.get().backupAllGroupSessions(info.version);
|
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: PHASE_DONE,
|
phase: PHASE_DONE,
|
||||||
});
|
});
|
||||||
|
@ -150,10 +158,24 @@ export default React.createClass({
|
||||||
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
this.setState({phase: PHASE_PASSPHRASE_CONFIRM});
|
||||||
},
|
},
|
||||||
|
|
||||||
_onPassPhraseKeyPress: function(e) {
|
_onPassPhraseKeyPress: async function(e) {
|
||||||
if (e.key === 'Enter' && this._passPhraseIsValid()) {
|
if (e.key === 'Enter') {
|
||||||
|
// If we're waiting for the timeout before updating the result at this point,
|
||||||
|
// skip ahead and do it now, otherwise we'll deny the attempt to proceed
|
||||||
|
// even if the user entered a valid passphrase
|
||||||
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
|
this._setZxcvbnResultTimeout = null;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
this.setState({
|
||||||
|
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||||
|
}, resolve);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this._passPhraseIsValid()) {
|
||||||
this._onPassPhraseNextClick();
|
this._onPassPhraseNextClick();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onPassPhraseConfirmNextClick: async function() {
|
_onPassPhraseConfirmNextClick: async function() {
|
||||||
|
@ -177,6 +199,7 @@ export default React.createClass({
|
||||||
passPhrase: '',
|
passPhrase: '',
|
||||||
passPhraseConfirm: '',
|
passPhraseConfirm: '',
|
||||||
phase: PHASE_PASSPHRASE,
|
phase: PHASE_PASSPHRASE,
|
||||||
|
zxcvbnResult: null,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -189,11 +212,20 @@ export default React.createClass({
|
||||||
_onPassPhraseChange: function(e) {
|
_onPassPhraseChange: function(e) {
|
||||||
this.setState({
|
this.setState({
|
||||||
passPhrase: e.target.value,
|
passPhrase: e.target.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this._setZxcvbnResultTimeout !== null) {
|
||||||
|
clearTimeout(this._setZxcvbnResultTimeout);
|
||||||
|
}
|
||||||
|
this._setZxcvbnResultTimeout = setTimeout(() => {
|
||||||
|
this._setZxcvbnResultTimeout = null;
|
||||||
|
this.setState({
|
||||||
// precompute this and keep it in state: zxcvbn is fast but
|
// precompute this and keep it in state: zxcvbn is fast but
|
||||||
// we use it in a couple of different places so no point recomputing
|
// we use it in a couple of different places so no point recomputing
|
||||||
// it unnecessarily.
|
// it unnecessarily.
|
||||||
zxcvbnResult: scorePassword(e.target.value),
|
zxcvbnResult: scorePassword(this.state.passPhrase),
|
||||||
});
|
});
|
||||||
|
}, PASSPHRASE_FEEDBACK_DELAY);
|
||||||
},
|
},
|
||||||
|
|
||||||
_onPassPhraseConfirmChange: function(e) {
|
_onPassPhraseConfirmChange: function(e) {
|
||||||
|
@ -246,6 +278,7 @@ export default React.createClass({
|
||||||
value={this.state.passPhrase}
|
value={this.state.passPhrase}
|
||||||
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
className="mx_CreateKeyBackupDialog_passPhraseInput"
|
||||||
placeholder={_t("Enter a passphrase...")}
|
placeholder={_t("Enter a passphrase...")}
|
||||||
|
autoFocus={true}
|
||||||
/>
|
/>
|
||||||
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
|
<div className="mx_CreateKeyBackupDialog_passPhraseHelp">
|
||||||
{strengthMeter}
|
{strengthMeter}
|
||||||
|
@ -294,14 +327,22 @@ export default React.createClass({
|
||||||
_renderPhasePassPhraseConfirm: function() {
|
_renderPhasePassPhraseConfirm: function() {
|
||||||
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
|
||||||
|
|
||||||
let passPhraseMatch = null;
|
|
||||||
if (this.state.passPhraseConfirm.length > 0) {
|
|
||||||
let matchText;
|
let matchText;
|
||||||
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
if (this.state.passPhraseConfirm === this.state.passPhrase) {
|
||||||
matchText = _t("That matches!");
|
matchText = _t("That matches!");
|
||||||
} else {
|
} else if (!this.state.passPhrase.startsWith(this.state.passPhraseConfirm)) {
|
||||||
|
// only tell them they're wrong if they've actually gone wrong.
|
||||||
|
// Security concious readers will note that if you left riot-web unattended
|
||||||
|
// on this screen, this would make it easy for a malicious person to guess
|
||||||
|
// your passphrase one letter at a time, but they could get this faster by
|
||||||
|
// just opening the browser's developer tools and reading it.
|
||||||
|
// Note that not having typed anything at all will not hit this clause and
|
||||||
|
// fall through so empty box === no hint.
|
||||||
matchText = _t("That doesn't match.");
|
matchText = _t("That doesn't match.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let passPhraseMatch = null;
|
||||||
|
if (matchText) {
|
||||||
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
passPhraseMatch = <div className="mx_CreateKeyBackupDialog_passPhraseMatch">
|
||||||
<div>{matchText}</div>
|
<div>{matchText}</div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -344,7 +385,10 @@ export default React.createClass({
|
||||||
_renderPhaseShowKey: function() {
|
_renderPhaseShowKey: function() {
|
||||||
let bodyText;
|
let bodyText;
|
||||||
if (this.state.setPassPhrase) {
|
if (this.state.setPassPhrase) {
|
||||||
bodyText = _t("As a safety net, you can use it to restore your encrypted message history if you forget your Recovery Passphrase.");
|
bodyText = _t(
|
||||||
|
"As a safety net, you can use it to restore your encrypted message " +
|
||||||
|
"history if you forget your Recovery Passphrase.",
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
bodyText = _t("As a safety net, you can use it to restore your encrypted message history.");
|
bodyText = _t("As a safety net, you can use it to restore your encrypted message history.");
|
||||||
}
|
}
|
||||||
|
@ -352,7 +396,7 @@ export default React.createClass({
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
|
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
|
||||||
<p>{bodyText}</p>
|
<p>{bodyText}</p>
|
||||||
<p className="mx_CreateKeyBackupDialog_primaryContainer">
|
<div className="mx_CreateKeyBackupDialog_primaryContainer">
|
||||||
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
|
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
|
||||||
{_t("Your Recovery Key")}
|
{_t("Your Recovery Key")}
|
||||||
</div>
|
</div>
|
||||||
|
@ -369,7 +413,7 @@ export default React.createClass({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -405,7 +449,6 @@ export default React.createClass({
|
||||||
_renderBusyPhase: function(text) {
|
_renderBusyPhase: function(text) {
|
||||||
const Spinner = sdk.getComponent('views.elements.Spinner');
|
const Spinner = sdk.getComponent('views.elements.Spinner');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t(text)}</p>
|
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
@ -413,8 +456,10 @@ export default React.createClass({
|
||||||
_renderPhaseDone: function() {
|
_renderPhaseDone: function() {
|
||||||
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
|
||||||
return <div>
|
return <div>
|
||||||
<p>{_t("Backup created")}</p>
|
<p>{_t(
|
||||||
<p>{_t("Your encryption keys are now being backed up to your Homeserver.")}</p>
|
"Your encryption keys are now being backed up in the background " +
|
||||||
|
"to your Homeserver. The initial backup could take several minutes. " +
|
||||||
|
"You can view key backup upload progress in Settings.")}</p>
|
||||||
<DialogButtons primaryButton={_t('Close')}
|
<DialogButtons primaryButton={_t('Close')}
|
||||||
onPrimaryButtonClick={this._onDone}
|
onPrimaryButtonClick={this._onDone}
|
||||||
hasCancel={false}
|
hasCancel={false}
|
||||||
|
@ -451,7 +496,9 @@ export default React.createClass({
|
||||||
case PHASE_KEEPITSAFE:
|
case PHASE_KEEPITSAFE:
|
||||||
return _t('Keep it safe');
|
return _t('Keep it safe');
|
||||||
case PHASE_BACKINGUP:
|
case PHASE_BACKINGUP:
|
||||||
return _t('Backing up...');
|
return _t('Starting backup...');
|
||||||
|
case PHASE_DONE:
|
||||||
|
return _t('Backup Started');
|
||||||
default:
|
default:
|
||||||
return _t("Create Key Backup");
|
return _t("Create Key Backup");
|
||||||
}
|
}
|
||||||
|
@ -488,7 +535,7 @@ export default React.createClass({
|
||||||
content = this._renderPhaseKeepItSafe();
|
content = this._renderPhaseKeepItSafe();
|
||||||
break;
|
break;
|
||||||
case PHASE_BACKINGUP:
|
case PHASE_BACKINGUP:
|
||||||
content = this._renderBusyPhase(_td("Backing up..."));
|
content = this._renderBusyPhase();
|
||||||
break;
|
break;
|
||||||
case PHASE_DONE:
|
case PHASE_DONE:
|
||||||
content = this._renderPhaseDone();
|
content = this._renderPhaseDone();
|
||||||
|
@ -503,7 +550,7 @@ export default React.createClass({
|
||||||
<BaseDialog className='mx_CreateKeyBackupDialog'
|
<BaseDialog className='mx_CreateKeyBackupDialog'
|
||||||
onFinished={this.props.onFinished}
|
onFinished={this.props.onFinished}
|
||||||
title={this._titleForPhase(this.state.phase)}
|
title={this._titleForPhase(this.state.phase)}
|
||||||
hasCancel={[PHASE_DONE].includes(this.state.phase)}
|
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{content}
|
{content}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018-2019 New Vector Ltd
|
||||||
|
|
||||||
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.
|
||||||
|
@ -24,9 +24,15 @@ import Modal from "../../../../Modal";
|
||||||
|
|
||||||
export default class NewRecoveryMethodDialog extends React.PureComponent {
|
export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
// As returned by js-sdk getKeyBackupVersion()
|
||||||
|
newVersionInfo: PropTypes.object,
|
||||||
onFinished: PropTypes.func.isRequired,
|
onFinished: PropTypes.func.isRequired,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOkClick = () => {
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
|
||||||
onGoToSettingsClick = () => {
|
onGoToSettingsClick = () => {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
dis.dispatch({ action: 'view_user_settings' });
|
dis.dispatch({ action: 'view_user_settings' });
|
||||||
|
@ -41,8 +47,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
// sending our own new keys to it.
|
// sending our own new keys to it.
|
||||||
let backupSigStatus;
|
let backupSigStatus;
|
||||||
try {
|
try {
|
||||||
const backupInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(this.props.newVersionInfo);
|
||||||
backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log("Unable to fetch key backup status", e);
|
console.log("Unable to fetch key backup status", e);
|
||||||
return;
|
return;
|
||||||
|
@ -71,39 +76,62 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
|
||||||
render() {
|
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 title = <span className="mx_NewRecoveryMethodDialog_title">
|
|
||||||
|
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||||
{_t("New Recovery Method")}
|
{_t("New Recovery Method")}
|
||||||
</span>;
|
</span>;
|
||||||
|
|
||||||
return (
|
const newMethodDetected = <p>{_t(
|
||||||
<BaseDialog className="mx_NewRecoveryMethodDialog"
|
|
||||||
onFinished={this.props.onFinished}
|
|
||||||
title={title}
|
|
||||||
hasCancel={false}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>{_t(
|
|
||||||
"A new recovery passphrase and key for Secure " +
|
"A new recovery passphrase and key for Secure " +
|
||||||
"Messages has been detected.",
|
"Messages have been detected.",
|
||||||
|
)}</p>;
|
||||||
|
|
||||||
|
const hackWarning = <p className="warning">{_t(
|
||||||
|
"If you didn't set the new recovery method, an " +
|
||||||
|
"attacker may be trying to access your account. " +
|
||||||
|
"Change your account password and set a new recovery " +
|
||||||
|
"method immediately in Settings.",
|
||||||
|
)}</p>;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||||
|
content = <div>
|
||||||
|
{newMethodDetected}
|
||||||
|
<p>{_t(
|
||||||
|
"This device is encrypting history using the new recovery method.",
|
||||||
)}</p>
|
)}</p>
|
||||||
|
{hackWarning}
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("OK")}
|
||||||
|
onPrimaryButtonClick={this.onOkClick}
|
||||||
|
cancelButton={_t("Go to Settings")}
|
||||||
|
onCancel={this.onGoToSettingsClick}
|
||||||
|
/>
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
content = <div>
|
||||||
|
{newMethodDetected}
|
||||||
<p>{_t(
|
<p>{_t(
|
||||||
"Setting up Secure Messages on this device " +
|
"Setting up Secure Messages on this device " +
|
||||||
"will re-encrypt this device's message history with " +
|
"will re-encrypt this device's message history with " +
|
||||||
"the new recovery method.",
|
"the new recovery method.",
|
||||||
)}</p>
|
)}</p>
|
||||||
<p className="warning">{_t(
|
{hackWarning}
|
||||||
"If you didn't set the new recovery method, an " +
|
|
||||||
"attacker may be trying to access your account. " +
|
|
||||||
"Change your account password and set a new recovery " +
|
|
||||||
"method immediately in Settings.",
|
|
||||||
)}</p>
|
|
||||||
<DialogButtons
|
<DialogButtons
|
||||||
primaryButton={_t("Set up Secure Messages")}
|
primaryButton={_t("Set up Secure Messages")}
|
||||||
onPrimaryButtonClick={this.onSetupClick}
|
onPrimaryButtonClick={this.onSetupClick}
|
||||||
cancelButton={_t("Go to Settings")}
|
cancelButton={_t("Go to Settings")}
|
||||||
onCancel={this.onGoToSettingsClick}
|
onCancel={this.onGoToSettingsClick}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className="mx_KeyBackupFailedDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{content}
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import PropTypes from "prop-types";
|
||||||
|
import sdk from "../../../../index";
|
||||||
|
import dis from "../../../../dispatcher";
|
||||||
|
import { _t } from "../../../../languageHandler";
|
||||||
|
import Modal from "../../../../Modal";
|
||||||
|
|
||||||
|
export default class RecoveryMethodRemovedDialog extends React.PureComponent {
|
||||||
|
static propTypes = {
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
}
|
||||||
|
|
||||||
|
onGoToSettingsClick = () => {
|
||||||
|
this.props.onFinished();
|
||||||
|
dis.dispatch({ action: 'view_user_settings' });
|
||||||
|
}
|
||||||
|
|
||||||
|
onSetupClick = () => {
|
||||||
|
this.props.onFinished();
|
||||||
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
|
import("./CreateKeyBackupDialog"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const BaseDialog = sdk.getComponent("views.dialogs.BaseDialog");
|
||||||
|
const DialogButtons = sdk.getComponent("views.elements.DialogButtons");
|
||||||
|
|
||||||
|
const title = <span className="mx_KeyBackupFailedDialog_title">
|
||||||
|
{_t("Recovery Method Removed")}
|
||||||
|
</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className="mx_KeyBackupFailedDialog"
|
||||||
|
onFinished={this.props.onFinished}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p>{_t(
|
||||||
|
"This device has detected that your recovery passphrase and key " +
|
||||||
|
"for Secure Messages have been removed.",
|
||||||
|
)}</p>
|
||||||
|
<p>{_t(
|
||||||
|
"If you did this accidentally, you can setup Secure Messages on " +
|
||||||
|
"this device which will re-encrypt this device's message " +
|
||||||
|
"history with a new recovery method.",
|
||||||
|
)}</p>
|
||||||
|
<p className="warning">{_t(
|
||||||
|
"If you didn't remove the recovery method, an " +
|
||||||
|
"attacker may be trying to access your account. " +
|
||||||
|
"Change your account password and set a new recovery " +
|
||||||
|
"method immediately in Settings.",
|
||||||
|
)}</p>
|
||||||
|
<DialogButtons
|
||||||
|
primaryButton={_t("Set up Secure Messages")}
|
||||||
|
onPrimaryButtonClick={this.onSetupClick}
|
||||||
|
cancelButton={_t("Go to Settings")}
|
||||||
|
onCancel={this.onGoToSettingsClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -41,8 +41,12 @@ export default class AutocompleteProvider {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
* Of the matched commands in the query, returns the first that contains or is contained by the selection, or null.
|
||||||
|
* @param {string} query The query string
|
||||||
|
* @param {SelectionRange} selection Selection to search
|
||||||
|
* @param {boolean} force True if the user is forcing completion
|
||||||
|
* @return {object} { command, range } where both objects fields are null if no match
|
||||||
*/
|
*/
|
||||||
getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false): ?string {
|
getCurrentCommand(query: string, selection: SelectionRange, force: boolean = false) {
|
||||||
let commandRegex = this.commandRegex;
|
let commandRegex = this.commandRegex;
|
||||||
|
|
||||||
if (force && this.shouldForceComplete()) {
|
if (force && this.shouldForceComplete()) {
|
||||||
|
|
|
@ -60,8 +60,8 @@ const PROVIDER_COMPLETION_TIMEOUT = 3000;
|
||||||
export default class Autocompleter {
|
export default class Autocompleter {
|
||||||
constructor(room: Room) {
|
constructor(room: Room) {
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.providers = PROVIDERS.map((p) => {
|
this.providers = PROVIDERS.map((Prov) => {
|
||||||
return new p(room);
|
return new Prov(room);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default class UserProvider extends AutocompleteProvider {
|
||||||
users: Array<RoomMember> = null;
|
users: Array<RoomMember> = null;
|
||||||
room: Room = null;
|
room: Room = null;
|
||||||
|
|
||||||
constructor(room) {
|
constructor(room: Room) {
|
||||||
super(USER_REGEX, FORCED_USER_REGEX);
|
super(USER_REGEX, FORCED_USER_REGEX);
|
||||||
this.room = room;
|
this.room = room;
|
||||||
this.matcher = new QueryMatcher([], {
|
this.matcher = new QueryMatcher([], {
|
||||||
|
|
127
src/components/structures/GroupGridView.js
Normal file
127
src/components/structures/GroupGridView.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
/*
|
||||||
|
Copyright 2017 Vector Creations Ltd.
|
||||||
|
Copyright 2017, 2018 New Vector Ltd.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import OpenRoomsStore from '../../stores/OpenRoomsStore';
|
||||||
|
import dis from '../../dispatcher';
|
||||||
|
import {_t} from '../../languageHandler';
|
||||||
|
import RoomView from './RoomView';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import MainSplit from './MainSplit';
|
||||||
|
import RightPanel from './RightPanel';
|
||||||
|
import RoomHeaderButtons from '../views/right_panel/RoomHeaderButtons';
|
||||||
|
|
||||||
|
export default class RoomGridView extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
roomStores: OpenRoomsStore.getRoomStores(),
|
||||||
|
activeRoomStore: OpenRoomsStore.getActiveRoomStore(),
|
||||||
|
};
|
||||||
|
this.onRoomsChanged = this.onRoomsChanged.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(_, prevState) {
|
||||||
|
const store = this.state.activeRoomStore;
|
||||||
|
if (store) {
|
||||||
|
store.getDispatcher().dispatch({action: 'focus_composer'});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.componentDidUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this._unmounted = false;
|
||||||
|
this._openRoomsStoreRegistration = OpenRoomsStore.addListener(this.onRoomsChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this._unmounted = true;
|
||||||
|
if (this._openRoomsStoreRegistration) {
|
||||||
|
this._openRoomsStoreRegistration.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRoomsChanged() {
|
||||||
|
if (this._unmounted) return;
|
||||||
|
this.setState({
|
||||||
|
roomStores: OpenRoomsStore.getRoomStores(),
|
||||||
|
activeRoomStore: OpenRoomsStore.getActiveRoomStore(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setActive(i) {
|
||||||
|
const store = OpenRoomsStore.getRoomStoreAt(i);
|
||||||
|
if (store !== this.state.activeRoomStore) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'group_grid_set_active',
|
||||||
|
room_id: store.getRoomId(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let roomStores = this.state.roomStores.slice(0, 6);
|
||||||
|
const emptyCount = 6 - roomStores.length;
|
||||||
|
if (emptyCount) {
|
||||||
|
const emptyTiles = Array.from({length: emptyCount}, () => null);
|
||||||
|
roomStores = roomStores.concat(emptyTiles);
|
||||||
|
}
|
||||||
|
const activeRoomId = this.state.activeRoomStore && this.state.activeRoomStore.getRoomId();
|
||||||
|
let rightPanel;
|
||||||
|
if (activeRoomId) {
|
||||||
|
rightPanel = (
|
||||||
|
<div className="mx_GroupGridView_rightPanel">
|
||||||
|
<div className="mx_GroupGridView_tabs"><RoomHeaderButtons /></div>
|
||||||
|
<RightPanel roomId={activeRoomId} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (<main className="mx_GroupGridView">
|
||||||
|
<MainSplit panel={rightPanel} collapsedRhs={this.props.collapsedRhs} >
|
||||||
|
<div className="mx_GroupGridView_rooms">
|
||||||
|
{ roomStores.map((roomStore, i) => {
|
||||||
|
if (roomStore) {
|
||||||
|
const isActive = roomStore === this.state.activeRoomStore;
|
||||||
|
const tileClasses = classNames({
|
||||||
|
"mx_GroupGridView_tile": true,
|
||||||
|
"mx_GroupGridView_activeTile": isActive,
|
||||||
|
});
|
||||||
|
return (<section
|
||||||
|
onClick={() => {this._setActive(i);}}
|
||||||
|
key={roomStore.getRoomId()}
|
||||||
|
className={tileClasses}
|
||||||
|
>
|
||||||
|
<RoomView
|
||||||
|
collapsedRhs={this.props.collapsedRhs}
|
||||||
|
isGrid={true}
|
||||||
|
roomViewStore={roomStore}
|
||||||
|
isActive={isActive}
|
||||||
|
/>
|
||||||
|
</section>);
|
||||||
|
} else {
|
||||||
|
return (<section className={"mx_GroupGridView_emptyTile"} key={`empty-${i}`}>{_t("No room in this tile yet.")}</section>);
|
||||||
|
}
|
||||||
|
}) }
|
||||||
|
</div>
|
||||||
|
</MainSplit>
|
||||||
|
</main>);
|
||||||
|
}
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import sessionStore from '../../stores/SessionStore';
|
||||||
import MatrixClientPeg from '../../MatrixClientPeg';
|
import MatrixClientPeg from '../../MatrixClientPeg';
|
||||||
import SettingsStore from "../../settings/SettingsStore";
|
import SettingsStore from "../../settings/SettingsStore";
|
||||||
import RoomListStore from "../../stores/RoomListStore";
|
import RoomListStore from "../../stores/RoomListStore";
|
||||||
|
import OpenRoomsStore from "../../stores/OpenRoomsStore";
|
||||||
|
|
||||||
import TagOrderActions from '../../actions/TagOrderActions';
|
import TagOrderActions from '../../actions/TagOrderActions';
|
||||||
import RoomListActions from '../../actions/RoomListActions';
|
import RoomListActions from '../../actions/RoomListActions';
|
||||||
|
@ -416,6 +417,7 @@ const LoggedInView = React.createClass({
|
||||||
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
|
||||||
const HomePage = sdk.getComponent('structures.HomePage');
|
const HomePage = sdk.getComponent('structures.HomePage');
|
||||||
const GroupView = sdk.getComponent('structures.GroupView');
|
const GroupView = sdk.getComponent('structures.GroupView');
|
||||||
|
const GroupGridView = sdk.getComponent('structures.GroupGridView');
|
||||||
const MyGroups = sdk.getComponent('structures.MyGroups');
|
const MyGroups = sdk.getComponent('structures.MyGroups');
|
||||||
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
|
||||||
const CookieBar = sdk.getComponent('globals.CookieBar');
|
const CookieBar = sdk.getComponent('globals.CookieBar');
|
||||||
|
@ -428,7 +430,14 @@ const LoggedInView = React.createClass({
|
||||||
|
|
||||||
switch (this.props.page_type) {
|
switch (this.props.page_type) {
|
||||||
case PageTypes.RoomView:
|
case PageTypes.RoomView:
|
||||||
|
if (!OpenRoomsStore.getActiveRoomStore()) {
|
||||||
|
console.warn(`LoggedInView: getCurrentRoomStore not set!`);
|
||||||
|
}
|
||||||
|
else if (OpenRoomsStore.getActiveRoomStore().getRoomId() !== this.props.currentRoomId) {
|
||||||
|
console.warn(`LoggedInView: room id in store not the same as in props: ${OpenRoomsStore.getActiveRoomStore().getRoomId()} & ${this.props.currentRoomId}`);
|
||||||
|
}
|
||||||
page_element = <RoomView
|
page_element = <RoomView
|
||||||
|
roomViewStore={OpenRoomsStore.getActiveRoomStore()}
|
||||||
ref='roomView'
|
ref='roomView'
|
||||||
autoJoin={this.props.autoJoin}
|
autoJoin={this.props.autoJoin}
|
||||||
onRegistered={this.props.onRegistered}
|
onRegistered={this.props.onRegistered}
|
||||||
|
@ -442,7 +451,9 @@ const LoggedInView = React.createClass({
|
||||||
ConferenceHandler={this.props.ConferenceHandler}
|
ConferenceHandler={this.props.ConferenceHandler}
|
||||||
/>;
|
/>;
|
||||||
break;
|
break;
|
||||||
|
case PageTypes.GroupGridView:
|
||||||
|
page_element = <GroupGridView collapsedRhs={this.props.collapsedRhs} />;
|
||||||
|
break;
|
||||||
case PageTypes.UserSettings:
|
case PageTypes.UserSettings:
|
||||||
page_element = <UserSettings
|
page_element = <UserSettings
|
||||||
onClose={this.props.onCloseAllSettings}
|
onClose={this.props.onCloseAllSettings}
|
||||||
|
|
|
@ -71,14 +71,13 @@ export default class MainSplit extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
|
const shouldAllowResizing =
|
||||||
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
|
!this.props.collapsedRhs &&
|
||||||
const wasPanelSet = this.props.panel && !prevProps.panel;
|
this.props.panel;
|
||||||
const wasPanelCleared = !this.props.panel && prevProps.panel;
|
|
||||||
|
|
||||||
if (wasExpanded || wasPanelSet) {
|
if (shouldAllowResizing && !this.resizer) {
|
||||||
this._createResizer();
|
this._createResizer();
|
||||||
} else if (wasCollapsed || wasPanelCleared) {
|
} else if (!shouldAllowResizing && this.resizer) {
|
||||||
this.resizer.detach();
|
this.resizer.detach();
|
||||||
this.resizer = null;
|
this.resizer = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2017, 2018 New Vector Ltd
|
Copyright 2017-2019 New Vector Ltd
|
||||||
|
|
||||||
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.
|
||||||
|
@ -651,6 +651,9 @@ export default React.createClass({
|
||||||
case 'view_group':
|
case 'view_group':
|
||||||
this._viewGroup(payload);
|
this._viewGroup(payload);
|
||||||
break;
|
break;
|
||||||
|
case 'group_grid_view':
|
||||||
|
this._viewGroupGrid(payload);
|
||||||
|
break;
|
||||||
case 'view_home_page':
|
case 'view_home_page':
|
||||||
this._viewHome();
|
this._viewHome();
|
||||||
break;
|
break;
|
||||||
|
@ -862,6 +865,7 @@ export default React.createClass({
|
||||||
// room name and avatar from an invite email)
|
// room name and avatar from an invite email)
|
||||||
_viewRoom: function(roomInfo) {
|
_viewRoom: function(roomInfo) {
|
||||||
this.focusComposer = true;
|
this.focusComposer = true;
|
||||||
|
console.log("!!! MatrixChat._viewRoom", roomInfo);
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
currentRoomId: roomInfo.room_id || null,
|
currentRoomId: roomInfo.room_id || null,
|
||||||
|
@ -910,6 +914,9 @@ export default React.createClass({
|
||||||
if (roomInfo.event_id && roomInfo.highlighted) {
|
if (roomInfo.event_id && roomInfo.highlighted) {
|
||||||
presentedId += "/" + roomInfo.event_id;
|
presentedId += "/" + roomInfo.event_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TODO: only emit this when we're not in grid mode?
|
||||||
this.notifyNewScreen('room/' + presentedId);
|
this.notifyNewScreen('room/' + presentedId);
|
||||||
newState.ready = true;
|
newState.ready = true;
|
||||||
this.setState(newState);
|
this.setState(newState);
|
||||||
|
@ -926,6 +933,11 @@ export default React.createClass({
|
||||||
this.notifyNewScreen('group/' + groupId);
|
this.notifyNewScreen('group/' + groupId);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_viewGroupGrid: function(payload) {
|
||||||
|
this._setPage(PageTypes.GroupGridView);
|
||||||
|
// this.notifyNewScreen('grid/' + payload.group_id);
|
||||||
|
},
|
||||||
|
|
||||||
_viewHome: function() {
|
_viewHome: function() {
|
||||||
// The home page requires the "logged in" view, so we'll set that.
|
// The home page requires the "logged in" view, so we'll set that.
|
||||||
this.setStateForNewView({
|
this.setStateForNewView({
|
||||||
|
@ -1435,10 +1447,33 @@ export default React.createClass({
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cli.on("crypto.keyBackupFailed", () => {
|
cli.on("crypto.keyBackupFailed", async (errcode) => {
|
||||||
|
let haveNewVersion;
|
||||||
|
let newVersionInfo;
|
||||||
|
// if key backup is still enabled, there must be a new backup in place
|
||||||
|
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||||
|
haveNewVersion = true;
|
||||||
|
} else {
|
||||||
|
// otherwise check the server to see if there's a new one
|
||||||
|
try {
|
||||||
|
newVersionInfo = await MatrixClientPeg.get().getKeyBackupVersion();
|
||||||
|
if (newVersionInfo !== null) haveNewVersion = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Saw key backup error but failed to check backup version!", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (haveNewVersion) {
|
||||||
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
|
Modal.createTrackedDialogAsync('New Recovery Method', 'New Recovery Method',
|
||||||
import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
|
import('../../async-components/views/dialogs/keybackup/NewRecoveryMethodDialog'),
|
||||||
|
{ newVersionInfo },
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
Modal.createTrackedDialogAsync('Recovery Method Removed', 'Recovery Method Removed',
|
||||||
|
import('../../async-components/views/dialogs/keybackup/RecoveryMethodRemovedDialog'),
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fire the tinter right on startup to ensure the default theme is applied
|
// Fire the tinter right on startup to ensure the default theme is applied
|
||||||
|
|
|
@ -165,7 +165,7 @@ export default class RightPanel extends React.Component {
|
||||||
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
|
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
|
||||||
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
|
||||||
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
} else if (this.state.phase === RightPanel.Phase.RoomMemberInfo) {
|
||||||
panel = <MemberInfo member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
panel = <MemberInfo roomId={this.props.roomId} member={this.state.member} key={this.props.roomId || this.state.member.userId} />;
|
||||||
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
} else if (this.state.phase === RightPanel.Phase.GroupMemberInfo) {
|
||||||
panel = <GroupMemberInfo
|
panel = <GroupMemberInfo
|
||||||
groupMember={this.state.member}
|
groupMember={this.state.member}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2015, 2016 OpenMarket Ltd
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
Copyright 2017 Vector Creations Ltd
|
Copyright 2017 Vector Creations Ltd
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
|
|
||||||
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.
|
||||||
|
@ -36,7 +36,6 @@ const ContentMessages = require("../../ContentMessages");
|
||||||
const Modal = require("../../Modal");
|
const Modal = require("../../Modal");
|
||||||
const sdk = require('../../index');
|
const sdk = require('../../index');
|
||||||
const CallHandler = require('../../CallHandler');
|
const CallHandler = require('../../CallHandler');
|
||||||
const dis = require("../../dispatcher");
|
|
||||||
const Tinter = require("../../Tinter");
|
const Tinter = require("../../Tinter");
|
||||||
const rate_limited_func = require('../../ratelimitedfunc');
|
const rate_limited_func = require('../../ratelimitedfunc');
|
||||||
const ObjectUtils = require('../../ObjectUtils');
|
const ObjectUtils = require('../../ObjectUtils');
|
||||||
|
@ -46,7 +45,6 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
|
||||||
|
|
||||||
import MainSplit from './MainSplit';
|
import MainSplit from './MainSplit';
|
||||||
import RightPanel from './RightPanel';
|
import RightPanel from './RightPanel';
|
||||||
import RoomViewStore from '../../stores/RoomViewStore';
|
|
||||||
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
|
||||||
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
import WidgetEchoStore from '../../stores/WidgetEchoStore';
|
||||||
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
|
||||||
|
@ -94,6 +92,8 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Servers the RoomView can use to try and assist joins
|
// Servers the RoomView can use to try and assist joins
|
||||||
viaServers: PropTypes.arrayOf(PropTypes.string),
|
viaServers: PropTypes.arrayOf(PropTypes.string),
|
||||||
|
// the store for this room view
|
||||||
|
roomViewStore: PropTypes.object.isRequired,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -155,7 +155,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillMount: function() {
|
componentWillMount: function() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction);
|
||||||
MatrixClientPeg.get().on("Room", this.onRoom);
|
MatrixClientPeg.get().on("Room", this.onRoom);
|
||||||
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().on("Room.timeline", this.onRoomTimeline);
|
||||||
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
MatrixClientPeg.get().on("Room.name", this.onRoomName);
|
||||||
|
@ -166,7 +166,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
|
||||||
this._fetchMediaConfig();
|
this._fetchMediaConfig();
|
||||||
// Start listening for RoomViewStore updates
|
// Start listening for RoomViewStore updates
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
this._onRoomViewStoreUpdate(true);
|
this._onRoomViewStoreUpdate(true);
|
||||||
|
|
||||||
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
|
||||||
|
@ -197,8 +197,8 @@ module.exports = React.createClass({
|
||||||
if (this.unmounted) {
|
if (this.unmounted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const store = this.props.roomViewStore;
|
||||||
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
|
if (!initial && this.state.roomId !== store.getRoomId()) {
|
||||||
// RoomView explicitly does not support changing what room
|
// RoomView explicitly does not support changing what room
|
||||||
// is being viewed: instead it should just be re-mounted when
|
// is being viewed: instead it should just be re-mounted when
|
||||||
// switching rooms. Therefore, if the room ID changes, we
|
// switching rooms. Therefore, if the room ID changes, we
|
||||||
|
@ -212,22 +212,21 @@ module.exports = React.createClass({
|
||||||
// it was, it means we're about to be unmounted.
|
// it was, it means we're about to be unmounted.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
roomId: RoomViewStore.getRoomId(),
|
roomId: store.getRoomId(),
|
||||||
roomAlias: RoomViewStore.getRoomAlias(),
|
roomAlias: store.getRoomAlias(),
|
||||||
roomLoading: RoomViewStore.isRoomLoading(),
|
roomLoading: store.isRoomLoading(),
|
||||||
roomLoadError: RoomViewStore.getRoomLoadError(),
|
roomLoadError: store.getRoomLoadError(),
|
||||||
joining: RoomViewStore.isJoining(),
|
joining: store.isJoining(),
|
||||||
initialEventId: RoomViewStore.getInitialEventId(),
|
initialEventId: store.getInitialEventId(),
|
||||||
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
|
isInitialEventHighlighted: store.isInitialEventHighlighted(),
|
||||||
forwardingEvent: RoomViewStore.getForwardingEvent(),
|
forwardingEvent: store.getForwardingEvent(),
|
||||||
shouldPeek: RoomViewStore.shouldPeek(),
|
shouldPeek: store.shouldPeek(),
|
||||||
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
|
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", store.getRoomId()),
|
||||||
editingRoomSettings: RoomViewStore.isEditingSettings(),
|
editingRoomSettings: store.isEditingSettings(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.editingRoomSettings && !newState.editingRoomSettings) dis.dispatch({action: 'focus_composer'});
|
if (this.state.editingRoomSettings && !newState.editingRoomSettings) this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
|
||||||
|
|
||||||
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
// Temporary logging to diagnose https://github.com/vector-im/riot-web/issues/4307
|
||||||
console.log(
|
console.log(
|
||||||
|
@ -389,7 +388,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
// XXX: EVIL HACK to autofocus inviting on empty rooms.
|
||||||
// We use the setTimeout to avoid racing with focus_composer.
|
// We use the setTimeout to avoid racing with focus_composer.
|
||||||
if (this.state.room &&
|
if (this.props.isActive !== false && this.state.room &&
|
||||||
this.state.room.getJoinedMemberCount() == 1 &&
|
this.state.room.getJoinedMemberCount() == 1 &&
|
||||||
this.state.room.getLiveTimeline() &&
|
this.state.room.getLiveTimeline() &&
|
||||||
this.state.room.getLiveTimeline().getEvents() &&
|
this.state.room.getLiveTimeline().getEvents() &&
|
||||||
|
@ -443,7 +442,7 @@ module.exports = React.createClass({
|
||||||
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
|
||||||
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
|
||||||
}
|
}
|
||||||
dis.unregister(this.dispatcherRef);
|
this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef);
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
MatrixClientPeg.get().removeListener("Room", this.onRoom);
|
||||||
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
|
||||||
|
@ -611,17 +610,10 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async onRoomRecoveryReminderFinished(backupCreated) {
|
onRoomRecoveryReminderDontAskAgain: function() {
|
||||||
// If the user cancelled the key backup dialog, it suggests they don't
|
// Called when the option to not ask again is set:
|
||||||
// want to be reminded anymore.
|
// force an update to hide the recovery reminder
|
||||||
if (!backupCreated) {
|
this.forceUpdate();
|
||||||
await SettingsStore.setValue(
|
|
||||||
"showRoomRecoveryReminder",
|
|
||||||
null,
|
|
||||||
SettingLevel.ACCOUNT,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
onKeyBackupStatus() {
|
onKeyBackupStatus() {
|
||||||
|
@ -842,7 +834,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchResultsResize: function() {
|
onSearchResultsResize: function() {
|
||||||
dis.dispatch({ action: 'timeline_resize' }, true);
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'timeline_resize' }, true);
|
||||||
},
|
},
|
||||||
|
|
||||||
onSearchResultsFillRequest: function(backwards) {
|
onSearchResultsFillRequest: function(backwards) {
|
||||||
|
@ -863,7 +855,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onInviteButtonClick: function() {
|
onInviteButtonClick: function() {
|
||||||
// call AddressPickerDialog
|
// call AddressPickerDialog
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'view_invite',
|
action: 'view_invite',
|
||||||
roomId: this.state.room.roomId,
|
roomId: this.state.room.roomId,
|
||||||
});
|
});
|
||||||
|
@ -885,7 +877,7 @@ module.exports = React.createClass({
|
||||||
// Join this room once the user has registered and logged in
|
// Join this room once the user has registered and logged in
|
||||||
const signUrl = this.props.thirdPartyInvite ?
|
const signUrl = this.props.thirdPartyInvite ?
|
||||||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'do_after_sync_prepared',
|
action: 'do_after_sync_prepared',
|
||||||
deferred_action: {
|
deferred_action: {
|
||||||
action: 'join_room',
|
action: 'join_room',
|
||||||
|
@ -895,7 +887,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
// Don't peek whilst registering otherwise getPendingEventList complains
|
// Don't peek whilst registering otherwise getPendingEventList complains
|
||||||
// Do this by indicating our intention to join
|
// Do this by indicating our intention to join
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'will_join',
|
action: 'will_join',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -906,20 +898,20 @@ module.exports = React.createClass({
|
||||||
if (submitted) {
|
if (submitted) {
|
||||||
this.props.onRegistered(credentials);
|
this.props.onRegistered(credentials);
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'cancel_after_sync_prepared',
|
action: 'cancel_after_sync_prepared',
|
||||||
});
|
});
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'cancel_join',
|
action: 'cancel_join',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onDifferentServerClicked: (ev) => {
|
onDifferentServerClicked: (ev) => {
|
||||||
dis.dispatch({action: 'start_registration'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'start_registration'});
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
onLoginClick: (ev) => {
|
onLoginClick: (ev) => {
|
||||||
dis.dispatch({action: 'start_login'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'start_login'});
|
||||||
close();
|
close();
|
||||||
},
|
},
|
||||||
}).close;
|
}).close;
|
||||||
|
@ -929,7 +921,7 @@ module.exports = React.createClass({
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const signUrl = this.props.thirdPartyInvite ?
|
const signUrl = this.props.thirdPartyInvite ?
|
||||||
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
this.props.thirdPartyInvite.inviteSignUrl : undefined;
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'join_room',
|
action: 'join_room',
|
||||||
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
|
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
|
||||||
});
|
});
|
||||||
|
@ -994,10 +986,10 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
uploadFile: async function(file) {
|
uploadFile: async function(file) {
|
||||||
dis.dispatch({action: 'focus_composer'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
|
||||||
|
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'require_registration'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1021,14 +1013,14 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
|
// Send message_sent callback, for things like _checkIfAlone because after all a file is still a message.
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
injectSticker: function(url, info, text) {
|
injectSticker: function(url, info, text) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'require_registration'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1229,7 +1221,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onSettingsClick: function() {
|
onSettingsClick: function() {
|
||||||
dis.dispatch({ action: 'open_room_settings' });
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'open_room_settings' });
|
||||||
},
|
},
|
||||||
|
|
||||||
onSettingsSaveClick: function() {
|
onSettingsSaveClick: function() {
|
||||||
|
@ -1262,31 +1254,31 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
// still editing room settings
|
// still editing room settings
|
||||||
} else {
|
} else {
|
||||||
dis.dispatch({ action: 'close_settings' });
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' });
|
||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
uploadingRoomSettings: false,
|
uploadingRoomSettings: false,
|
||||||
});
|
});
|
||||||
dis.dispatch({ action: 'close_settings' });
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' });
|
||||||
}).done();
|
}).done();
|
||||||
},
|
},
|
||||||
|
|
||||||
onCancelClick: function() {
|
onCancelClick: function() {
|
||||||
console.log("updateTint from onCancelClick");
|
console.log("updateTint from onCancelClick");
|
||||||
this.updateTint();
|
this.updateTint();
|
||||||
dis.dispatch({ action: 'close_settings' });
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' });
|
||||||
if (this.state.forwardingEvent) {
|
if (this.state.forwardingEvent) {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'forward_event',
|
action: 'forward_event',
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
dis.dispatch({action: 'focus_composer'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
onLeaveClick: function() {
|
onLeaveClick: function() {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'leave_room',
|
action: 'leave_room',
|
||||||
room_id: this.state.room.roomId,
|
room_id: this.state.room.roomId,
|
||||||
});
|
});
|
||||||
|
@ -1294,7 +1286,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
onForgetClick: function() {
|
onForgetClick: function() {
|
||||||
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
|
MatrixClientPeg.get().forget(this.state.room.roomId).done(function() {
|
||||||
dis.dispatch({ action: 'view_next_room' });
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_next_room' });
|
||||||
}, function(err) {
|
}, function(err) {
|
||||||
const errCode = err.errcode || _t("unknown error code");
|
const errCode = err.errcode || _t("unknown error code");
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
|
@ -1311,7 +1303,7 @@ module.exports = React.createClass({
|
||||||
rejecting: true,
|
rejecting: true,
|
||||||
});
|
});
|
||||||
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
|
MatrixClientPeg.get().leave(this.state.roomId).done(function() {
|
||||||
dis.dispatch({ action: 'view_next_room' });
|
this.props.roomViewStore.getDispatcher().dispatch({ action: 'view_next_room' });
|
||||||
self.setState({
|
self.setState({
|
||||||
rejecting: false,
|
rejecting: false,
|
||||||
});
|
});
|
||||||
|
@ -1337,7 +1329,7 @@ module.exports = React.createClass({
|
||||||
// using /leave rather than /join. In the short term though, we
|
// using /leave rather than /join. In the short term though, we
|
||||||
// just ignore them.
|
// just ignore them.
|
||||||
// https://github.com/vector-im/vector-web/issues/1134
|
// https://github.com/vector-im/vector-web/issues/1134
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'view_room_directory',
|
action: 'view_room_directory',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1356,7 +1348,7 @@ module.exports = React.createClass({
|
||||||
// jump down to the bottom of this room, where new events are arriving
|
// jump down to the bottom of this room, where new events are arriving
|
||||||
jumpToLiveTimeline: function() {
|
jumpToLiveTimeline: function() {
|
||||||
this.refs.messagePanel.jumpToLiveTimeline();
|
this.refs.messagePanel.jumpToLiveTimeline();
|
||||||
dis.dispatch({action: 'focus_composer'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
|
||||||
},
|
},
|
||||||
|
|
||||||
// jump up to wherever our read marker is
|
// jump up to wherever our read marker is
|
||||||
|
@ -1446,7 +1438,7 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
|
|
||||||
onFullscreenClick: function() {
|
onFullscreenClick: function() {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'video_fullscreen',
|
action: 'video_fullscreen',
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
}, true);
|
}, true);
|
||||||
|
@ -1571,6 +1563,7 @@ module.exports = React.createClass({
|
||||||
<RoomHeader ref="header"
|
<RoomHeader ref="header"
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
|
isGrid={this.props.isGrid}
|
||||||
collapsedRhs={this.props.collapsedRhs}
|
collapsedRhs={this.props.collapsedRhs}
|
||||||
/>
|
/>
|
||||||
<div className="mx_RoomView_body">
|
<div className="mx_RoomView_body">
|
||||||
|
@ -1617,6 +1610,7 @@ module.exports = React.createClass({
|
||||||
<div className="mx_RoomView">
|
<div className="mx_RoomView">
|
||||||
<RoomHeader
|
<RoomHeader
|
||||||
ref="header"
|
ref="header"
|
||||||
|
isGrid={this.props.isGrid}
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
collapsedRhs={this.props.collapsedRhs}
|
collapsedRhs={this.props.collapsedRhs}
|
||||||
/>
|
/>
|
||||||
|
@ -1704,7 +1698,7 @@ module.exports = React.createClass({
|
||||||
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
aux = <RoomUpgradeWarningBar room={this.state.room} />;
|
||||||
hideCancel = true;
|
hideCancel = true;
|
||||||
} else if (showRoomRecoveryReminder) {
|
} else if (showRoomRecoveryReminder) {
|
||||||
aux = <RoomRecoveryReminder onFinished={this.onRoomRecoveryReminderFinished} />;
|
aux = <RoomRecoveryReminder onDontAskAgainSet={this.onRoomRecoveryReminderDontAskAgain} />;
|
||||||
hideCancel = true;
|
hideCancel = true;
|
||||||
} else if (this.state.showingPinned) {
|
} else if (this.state.showingPinned) {
|
||||||
hideCancel = true; // has own cancel
|
hideCancel = true; // has own cancel
|
||||||
|
@ -1758,7 +1752,9 @@ module.exports = React.createClass({
|
||||||
if (canSpeak) {
|
if (canSpeak) {
|
||||||
messageComposer =
|
messageComposer =
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
|
roomViewStore={this.props.roomViewStore}
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
|
isGrid={this.props.isGrid}
|
||||||
onResize={this.onChildResize}
|
onResize={this.onChildResize}
|
||||||
uploadFile={this.uploadFile}
|
uploadFile={this.uploadFile}
|
||||||
callState={this.state.callState}
|
callState={this.state.callState}
|
||||||
|
@ -1885,11 +1881,14 @@ module.exports = React.createClass({
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const rightPanel = this.state.room ? <RightPanel roomId={this.state.room.roomId} /> : undefined;
|
const rightPanel = this.state.room && !this.props.isGrid ?
|
||||||
|
<RightPanel roomId={this.state.room.roomId} /> :
|
||||||
|
undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
|
||||||
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
|
||||||
|
isGrid={this.props.isGrid}
|
||||||
oobData={this.props.oobData}
|
oobData={this.props.oobData}
|
||||||
editing={this.state.editingRoomSettings}
|
editing={this.state.editingRoomSettings}
|
||||||
saving={this.state.uploadingRoomSettings}
|
saving={this.state.uploadingRoomSettings}
|
||||||
|
|
|
@ -86,6 +86,7 @@ const SIMPLE_SETTINGS = [
|
||||||
{ id: "pinMentionedRooms" },
|
{ id: "pinMentionedRooms" },
|
||||||
{ id: "pinUnreadRooms" },
|
{ id: "pinUnreadRooms" },
|
||||||
{ id: "showDeveloperTools" },
|
{ id: "showDeveloperTools" },
|
||||||
|
{ id: "alwaysRetryInvites" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// These settings must be defined in SettingsStore
|
// These settings must be defined in SettingsStore
|
||||||
|
|
|
@ -21,6 +21,7 @@ import dis from '../../../dispatcher';
|
||||||
import TagOrderActions from '../../../actions/TagOrderActions';
|
import TagOrderActions from '../../../actions/TagOrderActions';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
export default class TagTileContextMenu extends React.Component {
|
export default class TagTileContextMenu extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -34,6 +35,7 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
|
|
||||||
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
|
||||||
this._onRemoveClick = this._onRemoveClick.bind(this);
|
this._onRemoveClick = this._onRemoveClick.bind(this);
|
||||||
|
this._onViewAsGridClick = this._onViewAsGridClick.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onViewCommunityClick() {
|
_onViewCommunityClick() {
|
||||||
|
@ -53,8 +55,28 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
this.props.onFinished();
|
this.props.onFinished();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onViewAsGridClick() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'group_grid_view',
|
||||||
|
group_id: this.props.tag,
|
||||||
|
});
|
||||||
|
this.props.onFinished();
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
let gridViewOption;
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_gridview")) {
|
||||||
|
gridViewOption = (<div className="mx_TagTileContextMenu_item" onClick={this._onViewAsGridClick} >
|
||||||
|
<TintableSvg
|
||||||
|
className="mx_TagTileContextMenu_item_icon"
|
||||||
|
src="img/feather-icons/grid.svg"
|
||||||
|
width="15"
|
||||||
|
height="15"
|
||||||
|
/>
|
||||||
|
{ _t('View as Grid') }
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
return <div>
|
return <div>
|
||||||
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
|
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
|
||||||
<TintableSvg
|
<TintableSvg
|
||||||
|
@ -65,6 +87,7 @@ export default class TagTileContextMenu extends React.Component {
|
||||||
/>
|
/>
|
||||||
{ _t('View Community') }
|
{ _t('View Community') }
|
||||||
</div>
|
</div>
|
||||||
|
{ gridViewOption }
|
||||||
<hr className="mx_TagTileContextMenu_separator" />
|
<hr className="mx_TagTileContextMenu_separator" />
|
||||||
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
|
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
|
||||||
<img className="mx_TagTileContextMenu_item_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
<img className="mx_TagTileContextMenu_item_icon" src="img/icon_context_delete.svg" width="15" height="15" />
|
||||||
|
|
|
@ -389,6 +389,17 @@ module.exports = React.createClass({
|
||||||
const suggestedList = [];
|
const suggestedList = [];
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.room_id) {
|
if (result.room_id) {
|
||||||
|
const client = MatrixClientPeg.get();
|
||||||
|
const room = client.getRoom(result.room_id);
|
||||||
|
if (room) {
|
||||||
|
const tombstone = room.currentState.getStateEvents('m.room.tombstone', '');
|
||||||
|
if (tombstone && tombstone.getContent() && tombstone.getContent()["replacement_room"]) {
|
||||||
|
const replacementRoom = client.getRoom(tombstone.getContent()["replacement_room"]);
|
||||||
|
|
||||||
|
// Skip rooms with tombstones where we are also aware of the replacement room.
|
||||||
|
if (replacementRoom) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
suggestedList.push({
|
suggestedList.push({
|
||||||
addressType: 'mx-room-id',
|
addressType: 'mx-room-id',
|
||||||
address: result.room_id,
|
address: result.room_id,
|
||||||
|
|
81
src/components/views/dialogs/AskInviteAnywayDialog.js
Normal file
81
src/components/views/dialogs/AskInviteAnywayDialog.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
Copyright 2019 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import sdk from '../../../index';
|
||||||
|
import { _t } from '../../../languageHandler';
|
||||||
|
import {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
export default React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
unknownProfileUsers: PropTypes.array.isRequired, // [ {userId, errorText}... ]
|
||||||
|
onInviteAnyways: PropTypes.func.isRequired,
|
||||||
|
onGiveUp: PropTypes.func.isRequired,
|
||||||
|
onFinished: PropTypes.func.isRequired,
|
||||||
|
},
|
||||||
|
|
||||||
|
_onInviteClicked: function() {
|
||||||
|
this.props.onInviteAnyways();
|
||||||
|
this.props.onFinished(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onInviteNeverWarnClicked: function() {
|
||||||
|
SettingsStore.setValue("alwaysInviteUnknownUsers", null, SettingLevel.ACCOUNT, true);
|
||||||
|
this.props.onInviteAnyways();
|
||||||
|
this.props.onFinished(true);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onGiveUpClicked: function() {
|
||||||
|
this.props.onGiveUp();
|
||||||
|
this.props.onFinished(false);
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
|
||||||
|
|
||||||
|
const errorList = this.props.unknownProfileUsers
|
||||||
|
.map(address => <li key={address.userId}>{address.userId}: {address.errorText}</li>);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseDialog className='mx_RetryInvitesDialog'
|
||||||
|
onFinished={this._onGiveUpClicked}
|
||||||
|
title={_t('The following users may not exist')}
|
||||||
|
contentId='mx_Dialog_content'
|
||||||
|
>
|
||||||
|
<div id='mx_Dialog_content'>
|
||||||
|
<p>{_t("The following users may not exist - would you like to invite them anyways?")}</p>
|
||||||
|
<ul>
|
||||||
|
{ errorList }
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx_Dialog_buttons">
|
||||||
|
<button onClick={this._onGiveUpClicked}>
|
||||||
|
{ _t('Close') }
|
||||||
|
</button>
|
||||||
|
<button onClick={this._onInviteNeverWarnClicked}>
|
||||||
|
{ _t('Invite anyways and never warn me again') }
|
||||||
|
</button>
|
||||||
|
<button onClick={this._onInviteClicked} autoFocus="true">
|
||||||
|
{ _t('Invite anyways') }
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</BaseDialog>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -78,7 +78,6 @@ export default class HeaderButtons extends React.Component {
|
||||||
// till show_right_panel, just without the fromHeader flag
|
// till show_right_panel, just without the fromHeader flag
|
||||||
// as that would hide the right panel again
|
// as that would hide the right panel again
|
||||||
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
|
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
|
||||||
|
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
phase: payload.phase,
|
phase: payload.phase,
|
||||||
|
|
|
@ -24,6 +24,8 @@ import ObjectUtils from '../../../ObjectUtils';
|
||||||
import AppsDrawer from './AppsDrawer';
|
import AppsDrawer from './AppsDrawer';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -60,6 +62,22 @@ module.exports = React.createClass({
|
||||||
hideAppsDrawer: false,
|
hideAppsDrawer: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return { counters: this._computeCounters() };
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
cli.on("RoomState.events", this._rateLimitedUpdate);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
const cli = MatrixClientPeg.get();
|
||||||
|
if (cli) {
|
||||||
|
cli.removeListener("RoomState.events", this._rateLimitedUpdate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
shouldComponentUpdate: function(nextProps, nextState) {
|
shouldComponentUpdate: function(nextProps, nextState) {
|
||||||
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
return (!ObjectUtils.shallowEqual(this.props, nextProps) ||
|
||||||
!ObjectUtils.shallowEqual(this.state, nextState));
|
!ObjectUtils.shallowEqual(this.state, nextState));
|
||||||
|
@ -82,6 +100,43 @@ module.exports = React.createClass({
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
_rateLimitedUpdate: new RateLimitedFunc(function() {
|
||||||
|
if (SettingsStore.isFeatureEnabled("feature_state_counters")) {
|
||||||
|
this.setState({counters: this._computeCounters()});
|
||||||
|
}
|
||||||
|
}, 500),
|
||||||
|
|
||||||
|
_computeCounters: function() {
|
||||||
|
let counters = [];
|
||||||
|
|
||||||
|
if (this.props.room && SettingsStore.isFeatureEnabled("feature_state_counters")) {
|
||||||
|
const stateEvs = this.props.room.currentState.getStateEvents('re.jki.counter');
|
||||||
|
stateEvs.sort((a, b) => {
|
||||||
|
return a.getStateKey() < b.getStateKey();
|
||||||
|
});
|
||||||
|
|
||||||
|
stateEvs.forEach((ev, idx) => {
|
||||||
|
const title = ev.getContent().title;
|
||||||
|
const value = ev.getContent().value;
|
||||||
|
const link = ev.getContent().link;
|
||||||
|
const severity = ev.getContent().severity || "normal";
|
||||||
|
const stateKey = ev.getStateKey();
|
||||||
|
|
||||||
|
if (title && value && severity) {
|
||||||
|
counters.push({
|
||||||
|
"title": title,
|
||||||
|
"value": value,
|
||||||
|
"link": link,
|
||||||
|
"severity": severity,
|
||||||
|
"stateKey": stateKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return counters;
|
||||||
|
},
|
||||||
|
|
||||||
render: function() {
|
render: function() {
|
||||||
const CallView = sdk.getComponent("voip.CallView");
|
const CallView = sdk.getComponent("voip.CallView");
|
||||||
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
const TintableSvg = sdk.getComponent("elements.TintableSvg");
|
||||||
|
@ -145,6 +200,58 @@ module.exports = React.createClass({
|
||||||
hide={this.props.hideAppsDrawer}
|
hide={this.props.hideAppsDrawer}
|
||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
let stateViews = null;
|
||||||
|
if (this.state.counters && SettingsStore.isFeatureEnabled("feature_state_counters")) {
|
||||||
|
let counters = [];
|
||||||
|
|
||||||
|
this.state.counters.forEach((counter, idx) => {
|
||||||
|
const title = counter.title;
|
||||||
|
const value = counter.value;
|
||||||
|
const link = counter.link;
|
||||||
|
const severity = counter.severity;
|
||||||
|
const stateKey = counter.stateKey;
|
||||||
|
|
||||||
|
if (title && value && severity) {
|
||||||
|
let span = <span>{ title }: { value }</span>
|
||||||
|
|
||||||
|
if (link) {
|
||||||
|
span = (
|
||||||
|
<a href={link} target="_blank" rel="noopener">
|
||||||
|
{ span }
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
span = (
|
||||||
|
<span
|
||||||
|
className="m_RoomView_auxPanel_stateViews_span"
|
||||||
|
data-severity={severity}
|
||||||
|
key={ "x-" + stateKey }
|
||||||
|
>
|
||||||
|
{span}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
counters.push(span);
|
||||||
|
counters.push(
|
||||||
|
<span
|
||||||
|
className="m_RoomView_auxPanel_stateViews_delim"
|
||||||
|
key={"delim" + idx}
|
||||||
|
> ─ </span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (counters.length > 0) {
|
||||||
|
counters.pop(); // remove last deliminator
|
||||||
|
stateViews = (
|
||||||
|
<div className="m_RoomView_auxPanel_stateViews">
|
||||||
|
{ counters }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const classes = classNames({
|
const classes = classNames({
|
||||||
"mx_RoomView_auxPanel": true,
|
"mx_RoomView_auxPanel": true,
|
||||||
"mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
|
"mx_RoomView_auxPanel_fullHeight": this.props.fullHeight,
|
||||||
|
@ -156,6 +263,7 @@ module.exports = React.createClass({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes} style={style} >
|
<div className={classes} style={style} >
|
||||||
|
{ stateViews }
|
||||||
{ appsDrawer }
|
{ appsDrawer }
|
||||||
{ fileDropTarget }
|
{ fileDropTarget }
|
||||||
{ callView }
|
{ callView }
|
||||||
|
|
|
@ -62,6 +62,7 @@ const stateEventTileTypes = {
|
||||||
'm.room.pinned_events': 'messages.TextualEvent',
|
'm.room.pinned_events': 'messages.TextualEvent',
|
||||||
'm.room.server_acl': 'messages.TextualEvent',
|
'm.room.server_acl': 'messages.TextualEvent',
|
||||||
'im.vector.modular.widgets': 'messages.TextualEvent',
|
'im.vector.modular.widgets': 'messages.TextualEvent',
|
||||||
|
'm.room.tombstone': 'messages.TextualEvent',
|
||||||
};
|
};
|
||||||
|
|
||||||
function getHandlerTile(ev) {
|
function getHandlerTile(ev) {
|
||||||
|
|
|
@ -39,7 +39,6 @@ import Unread from '../../../Unread';
|
||||||
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
|
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
|
||||||
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
import withMatrixClient from '../../../wrappers/withMatrixClient';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
|
||||||
import SdkConfig from '../../../SdkConfig';
|
import SdkConfig from '../../../SdkConfig';
|
||||||
import MultiInviter from "../../../utils/MultiInviter";
|
import MultiInviter from "../../../utils/MultiInviter";
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
@ -50,6 +49,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
propTypes: {
|
propTypes: {
|
||||||
matrixClient: PropTypes.object.isRequired,
|
matrixClient: PropTypes.object.isRequired,
|
||||||
member: PropTypes.object.isRequired,
|
member: PropTypes.object.isRequired,
|
||||||
|
roomId: PropTypes.string,
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -713,7 +713,7 @@ module.exports = withMatrixClient(React.createClass({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!member || !member.membership || member.membership === 'leave') {
|
if (!member || !member.membership || member.membership === 'leave') {
|
||||||
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
|
const roomId = member && member.roomId ? member.roomId : this.props.roomId;
|
||||||
const onInviteUserButton = async () => {
|
const onInviteUserButton = async () => {
|
||||||
try {
|
try {
|
||||||
// We use a MultiInviter to re-use the invite logic, even though
|
// We use a MultiInviter to re-use the invite logic, even though
|
||||||
|
|
|
@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import Stickerpicker from './Stickerpicker';
|
import Stickerpicker from './Stickerpicker';
|
||||||
import { makeRoomPermalink } from '../../../matrix-to';
|
import { makeRoomPermalink } from '../../../matrix-to';
|
||||||
|
@ -63,7 +62,7 @@ export default class MessageComposer extends React.Component {
|
||||||
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
|
||||||
},
|
},
|
||||||
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
|
||||||
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
|
isQuoting: Boolean(this.props.roomViewStore.getQuotingEvent()),
|
||||||
tombstone: this._getRoomTombstone(),
|
tombstone: this._getRoomTombstone(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -75,7 +74,7 @@ export default class MessageComposer extends React.Component {
|
||||||
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
// XXX: fragile as all hell - fixme somehow, perhaps with a dedicated Room.encryption event or something.
|
||||||
MatrixClientPeg.get().on("event", this.onEvent);
|
MatrixClientPeg.get().on("event", this.onEvent);
|
||||||
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
this._waitForOwnMember();
|
this._waitForOwnMember();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,14 +123,14 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomViewStoreUpdate() {
|
_onRoomViewStoreUpdate() {
|
||||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent());
|
||||||
if (this.state.isQuoting === isQuoting) return;
|
if (this.state.isQuoting === isQuoting) return;
|
||||||
this.setState({ isQuoting });
|
this.setState({ isQuoting });
|
||||||
}
|
}
|
||||||
|
|
||||||
onUploadClick(ev) {
|
onUploadClick(ev) {
|
||||||
if (MatrixClientPeg.get().isGuest()) {
|
if (MatrixClientPeg.get().isGuest()) {
|
||||||
dis.dispatch({action: 'require_registration'});
|
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,7 +164,7 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
|
const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent());
|
||||||
let replyToWarning = null;
|
let replyToWarning = null;
|
||||||
if (isQuoting) {
|
if (isQuoting) {
|
||||||
replyToWarning = <p>{
|
replyToWarning = <p>{
|
||||||
|
@ -229,7 +228,7 @@ export default class MessageComposer extends React.Component {
|
||||||
if (!call) {
|
if (!call) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'hangup',
|
action: 'hangup',
|
||||||
// hangup the call for this room, which may not be the room in props
|
// hangup the call for this room, which may not be the room in props
|
||||||
// (e.g. conferences which will hangup the 1:1 room instead)
|
// (e.g. conferences which will hangup the 1:1 room instead)
|
||||||
|
@ -238,7 +237,7 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onCallClick(ev) {
|
onCallClick(ev) {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: ev.shiftKey ? "screensharing" : "video",
|
type: ev.shiftKey ? "screensharing" : "video",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
|
@ -246,7 +245,7 @@ export default class MessageComposer extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onVoiceCallClick(ev) {
|
onVoiceCallClick(ev) {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'place_call',
|
action: 'place_call',
|
||||||
type: "voice",
|
type: "voice",
|
||||||
room_id: this.props.room.roomId,
|
room_id: this.props.room.roomId,
|
||||||
|
@ -282,10 +281,22 @@ export default class MessageComposer extends React.Component {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
|
|
||||||
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
|
||||||
dis.dispatch({
|
const replacementRoom = MatrixClientPeg.get().getRoom(replacementRoomId);
|
||||||
|
let createEventId = null;
|
||||||
|
if (replacementRoom) {
|
||||||
|
const createEvent = replacementRoom.currentState.getStateEvents('m.room.create', '');
|
||||||
|
if (createEvent && createEvent.getId()) createEventId = createEvent.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'view_room',
|
action: 'view_room',
|
||||||
highlighted: true,
|
highlighted: true,
|
||||||
|
event_id: createEventId,
|
||||||
room_id: replacementRoomId,
|
room_id: replacementRoomId,
|
||||||
|
|
||||||
|
// Try to join via the server that sent the event. This converts $something:example.org
|
||||||
|
// into a server domain by splitting on colons and ignoring the first entry ("$something").
|
||||||
|
via_servers: [this.state.tombstone.getId().split(':').splice(1).join(':')],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,8 +432,10 @@ export default class MessageComposer extends React.Component {
|
||||||
|
|
||||||
controls.push(
|
controls.push(
|
||||||
<MessageComposerInput
|
<MessageComposerInput
|
||||||
|
roomViewStore={this.props.roomViewStore}
|
||||||
ref={(c) => this.messageComposerInput = c}
|
ref={(c) => this.messageComposerInput = c}
|
||||||
key="controls_input"
|
key="controls_input"
|
||||||
|
isGrid={this.props.isGrid}
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
placeholder={placeholderText}
|
placeholder={placeholderText}
|
||||||
|
@ -529,5 +542,6 @@ MessageComposer.propTypes = {
|
||||||
uploadAllowed: PropTypes.func.isRequired,
|
uploadAllowed: PropTypes.func.isRequired,
|
||||||
|
|
||||||
// string representing the current room app drawer state
|
// string representing the current room app drawer state
|
||||||
showApps: PropTypes.bool
|
showApps: PropTypes.bool,
|
||||||
|
roomViewStore: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,13 +15,11 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import type SyntheticKeyboardEvent from 'react/lib/SyntheticKeyboardEvent';
|
|
||||||
|
|
||||||
import { Editor } from 'slate-react';
|
import { Editor } from 'slate-react';
|
||||||
import { getEventTransfer } from 'slate-react';
|
import { getEventTransfer } from 'slate-react';
|
||||||
import { Value, Document, Block, Inline, Text, Range, Node } from 'slate';
|
import { Value, Block, Inline, Range } from 'slate';
|
||||||
import type { Change } from 'slate';
|
import type { Change } from 'slate';
|
||||||
|
|
||||||
import Html from 'slate-html-serializer';
|
import Html from 'slate-html-serializer';
|
||||||
|
@ -30,7 +28,6 @@ import Plain from 'slate-plain-serializer';
|
||||||
import PlainWithPillsSerializer from "../../../autocomplete/PlainWithPillsSerializer";
|
import PlainWithPillsSerializer from "../../../autocomplete/PlainWithPillsSerializer";
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Promise from 'bluebird';
|
|
||||||
|
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
import type {MatrixClient} from 'matrix-js-sdk/lib/matrix';
|
||||||
|
@ -38,11 +35,9 @@ import {processCommandInput} from '../../../SlashCommands';
|
||||||
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../../Keyboard';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t, _td } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Analytics from '../../../Analytics';
|
import Analytics from '../../../Analytics';
|
||||||
|
|
||||||
import dis from '../../../dispatcher';
|
|
||||||
|
|
||||||
import * as RichText from '../../../RichText';
|
import * as RichText from '../../../RichText';
|
||||||
import * as HtmlUtils from '../../../HtmlUtils';
|
import * as HtmlUtils from '../../../HtmlUtils';
|
||||||
import Autocomplete from './Autocomplete';
|
import Autocomplete from './Autocomplete';
|
||||||
|
@ -51,28 +46,24 @@ import Markdown from '../../../Markdown';
|
||||||
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
import ComposerHistoryManager from '../../../ComposerHistoryManager';
|
||||||
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
import MessageComposerStore from '../../../stores/MessageComposerStore';
|
||||||
|
|
||||||
import {MATRIXTO_MD_LINK_PATTERN, MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
import {MATRIXTO_URL_PATTERN} from '../../../linkify-matrix';
|
||||||
const REGEX_MATRIXTO_MARKDOWN_GLOBAL = new RegExp(MATRIXTO_MD_LINK_PATTERN, 'g');
|
|
||||||
|
|
||||||
import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList, mapUnicodeToShort, toShort} from 'emojione';
|
import {
|
||||||
|
asciiRegexp, unicodeRegexp, shortnameToUnicode,
|
||||||
|
asciiList, mapUnicodeToShort, toShort,
|
||||||
|
} from 'emojione';
|
||||||
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
import {makeUserPermalink} from "../../../matrix-to";
|
import {makeUserPermalink} from "../../../matrix-to";
|
||||||
import ReplyPreview from "./ReplyPreview";
|
import ReplyPreview from "./ReplyPreview";
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
|
||||||
import ReplyThread from "../elements/ReplyThread";
|
import ReplyThread from "../elements/ReplyThread";
|
||||||
import {ContentHelpers} from 'matrix-js-sdk';
|
import {ContentHelpers} from 'matrix-js-sdk';
|
||||||
|
|
||||||
const EMOJI_SHORTNAMES = Object.keys(emojioneList);
|
|
||||||
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
const EMOJI_UNICODE_TO_SHORTNAME = mapUnicodeToShort();
|
||||||
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
const REGEX_EMOJI_WHITESPACE = new RegExp('(?:^|\\s)(' + asciiRegexp + ')\\s$');
|
||||||
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
const EMOJI_REGEX = new RegExp(unicodeRegexp, 'g');
|
||||||
|
|
||||||
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
const TYPING_USER_TIMEOUT = 10000; const TYPING_SERVER_TIMEOUT = 30000;
|
||||||
|
|
||||||
const ENTITY_TYPES = {
|
|
||||||
AT_ROOM_PILL: 'ATROOMPILL',
|
|
||||||
};
|
|
||||||
|
|
||||||
// the Slate node type to default to for unstyled text
|
// the Slate node type to default to for unstyled text
|
||||||
const DEFAULT_NODE = 'paragraph';
|
const DEFAULT_NODE = 'paragraph';
|
||||||
|
|
||||||
|
@ -117,15 +108,6 @@ const SLATE_SCHEMA = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function onSendMessageFailed(err, room) {
|
|
||||||
// XXX: temporary logging to try to diagnose
|
|
||||||
// https://github.com/vector-im/riot-web/issues/3148
|
|
||||||
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'message_send_failed',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function rangeEquals(a: Range, b: Range): boolean {
|
function rangeEquals(a: Range, b: Range): boolean {
|
||||||
return (a.anchor.key === b.anchor.key
|
return (a.anchor.key === b.anchor.key
|
||||||
&& a.anchor.offset === b.anchorOffset
|
&& a.anchor.offset === b.anchorOffset
|
||||||
|
@ -135,6 +117,18 @@ function rangeEquals(a: Range, b: Range): boolean {
|
||||||
&& a.isBackward === b.isBackward);
|
&& a.isBackward === b.isBackward);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class NoopHistoryManager {
|
||||||
|
getItem() {}
|
||||||
|
save() {}
|
||||||
|
|
||||||
|
get currentIndex() { return 0; }
|
||||||
|
set currentIndex(_) {}
|
||||||
|
|
||||||
|
get history() { return []; }
|
||||||
|
set history(_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The textInput part of the MessageComposer
|
* The textInput part of the MessageComposer
|
||||||
*/
|
*/
|
||||||
|
@ -150,6 +144,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
onFilesPasted: PropTypes.func,
|
onFilesPasted: PropTypes.func,
|
||||||
|
|
||||||
onInputStateChanged: PropTypes.func,
|
onInputStateChanged: PropTypes.func,
|
||||||
|
roomViewStore: PropTypes.object.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
|
@ -344,20 +339,32 @@ export default class MessageComposerInput extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
this.dispatcherRef = dis.register(this.onAction);
|
this.dispatcherRef = this.props.roomViewStore.getDispatcher().register(this.onAction);
|
||||||
|
if (this.props.isGrid) {
|
||||||
|
this.historyManager = new NoopHistoryManager();
|
||||||
|
} else {
|
||||||
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
dis.unregister(this.dispatcherRef);
|
this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef);
|
||||||
}
|
}
|
||||||
|
|
||||||
_collectEditor = (e) => {
|
_collectEditor = (e) => {
|
||||||
this._editor = e;
|
this._editor = e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSendMessageFailed = (err, room) => {
|
||||||
|
// XXX: temporary logging to try to diagnose
|
||||||
|
// https://github.com/vector-im/riot-web/issues/3148
|
||||||
|
console.log('MessageComposer got send failure: ' + err.name + '('+err+')');
|
||||||
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
|
action: 'message_send_failed',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
onAction = (payload) => {
|
onAction = (payload) => {
|
||||||
const editor = this._editor;
|
|
||||||
const editorState = this.state.editorState;
|
const editorState = this.state.editorState;
|
||||||
|
|
||||||
switch (payload.action) {
|
switch (payload.action) {
|
||||||
|
@ -854,7 +861,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newState: ?Value = null;
|
//const newState: ?Value = null;
|
||||||
|
|
||||||
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
// Draft handles rich text mode commands by default but we need to do it ourselves for Markdown.
|
||||||
if (this.state.isRichTextEnabled) {
|
if (this.state.isRichTextEnabled) {
|
||||||
|
@ -1105,7 +1112,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
|
||||||
Modal.createTrackedDialog('Server error', '', ErrorDialog, {
|
Modal.createTrackedDialog('Server error', '', ErrorDialog, {
|
||||||
title: _t("Server error"),
|
title: _t("Server error"),
|
||||||
description: ((err && err.message) ? err.message : _t("Server unavailable, overloaded, or something else went wrong.")),
|
description: ((err && err.message) ? err.message : _t(
|
||||||
|
"Server unavailable, overloaded, or something else went wrong.",
|
||||||
|
)),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else if (cmd.error) {
|
} else if (cmd.error) {
|
||||||
|
@ -1120,7 +1129,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const replyingToEv = RoomViewStore.getQuotingEvent();
|
const replyingToEv = this.props.roomViewStore.getQuotingEvent();
|
||||||
const mustSendHTML = Boolean(replyingToEv);
|
const mustSendHTML = Boolean(replyingToEv);
|
||||||
|
|
||||||
if (this.state.isRichTextEnabled) {
|
if (this.state.isRichTextEnabled) {
|
||||||
|
@ -1208,18 +1217,18 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
// Clear reply_to_event as we put the message into the queue
|
// Clear reply_to_event as we put the message into the queue
|
||||||
// if the send fails, retry will handle resending.
|
// if the send fails, retry will handle resending.
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'reply_to_event',
|
action: 'reply_to_event',
|
||||||
event: null,
|
event: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.client.sendMessage(this.props.room.roomId, content).then((res) => {
|
this.client.sendMessage(this.props.room.roomId, content).then((res) => {
|
||||||
dis.dispatch({
|
this.props.roomViewStore.getDispatcher().dispatch({
|
||||||
action: 'message_sent',
|
action: 'message_sent',
|
||||||
});
|
});
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
onSendMessageFailed(e, this.props.room);
|
this.onSendMessageFailed(e, this.props.room);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
|
@ -1484,7 +1493,9 @@ export default class MessageComposerInput extends React.Component {
|
||||||
});
|
});
|
||||||
const style = {};
|
const style = {};
|
||||||
if (props.selected) style.border = '1px solid blue';
|
if (props.selected) style.border = '1px solid blue';
|
||||||
return <img className={ className } src={ uri } title={ shortname } alt={ emojiUnicode } style={style} />;
|
return <img className={ className } src={ uri }
|
||||||
|
title={ shortname } alt={ emojiUnicode } style={style}
|
||||||
|
/>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1538,7 +1549,6 @@ export default class MessageComposerInput extends React.Component {
|
||||||
|
|
||||||
getSelectionRange(editorState: Value) {
|
getSelectionRange(editorState: Value) {
|
||||||
let beginning = false;
|
let beginning = false;
|
||||||
const query = this.getAutocompleteQuery(editorState);
|
|
||||||
const firstChild = editorState.document.nodes.get(0);
|
const firstChild = editorState.document.nodes.get(0);
|
||||||
const firstGrandChild = firstChild && firstChild.nodes.get(0);
|
const firstGrandChild = firstChild && firstChild.nodes.get(0);
|
||||||
beginning = (firstChild && firstGrandChild &&
|
beginning = (firstChild && firstGrandChild &&
|
||||||
|
@ -1589,7 +1599,7 @@ export default class MessageComposerInput extends React.Component {
|
||||||
return (
|
return (
|
||||||
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
|
||||||
<div className="mx_MessageComposer_autocomplete_wrapper">
|
<div className="mx_MessageComposer_autocomplete_wrapper">
|
||||||
<ReplyPreview />
|
<ReplyPreview roomViewStore={this.props.roomViewStore} />
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
ref={(e) => this.autocomplete = e}
|
ref={(e) => this.autocomplete = e}
|
||||||
room={this.props.room}
|
room={this.props.room}
|
||||||
|
|
|
@ -18,7 +18,6 @@ import React from 'react';
|
||||||
import dis from '../../../dispatcher';
|
import dis from '../../../dispatcher';
|
||||||
import sdk from '../../../index';
|
import sdk from '../../../index';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
function cancelQuoting() {
|
function cancelQuoting() {
|
||||||
|
@ -38,7 +37,7 @@ export default class ReplyPreview extends React.Component {
|
||||||
|
|
||||||
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
|
||||||
|
|
||||||
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
|
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
|
||||||
this._onRoomViewStoreUpdate();
|
this._onRoomViewStoreUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ export default class ReplyPreview extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
_onRoomViewStoreUpdate() {
|
_onRoomViewStoreUpdate() {
|
||||||
const event = RoomViewStore.getQuotingEvent();
|
const event = this.props.roomViewStore.getQuotingEvent();
|
||||||
if (this.state.event !== event) {
|
if (this.state.event !== event) {
|
||||||
this.setState({ event });
|
this.setState({ event });
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
|
||||||
import MatrixClientPeg from '../../../MatrixClientPeg';
|
import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import RateLimitedFunc from '../../../ratelimitedfunc';
|
import RateLimitedFunc from '../../../ratelimitedfunc';
|
||||||
|
import dis from '../../../dispatcher';
|
||||||
|
|
||||||
import * as linkify from 'linkifyjs';
|
import * as linkify from 'linkifyjs';
|
||||||
import linkifyElement from 'linkifyjs/element';
|
import linkifyElement from 'linkifyjs/element';
|
||||||
|
@ -152,6 +153,14 @@ module.exports = React.createClass({
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onToggleRightPanelClick: function(ev) {
|
||||||
|
if (this.props.collapsedRhs) {
|
||||||
|
dis.dispatch({action: "show_right_panel"});
|
||||||
|
} else {
|
||||||
|
dis.dispatch({action: "hide_right_panel"});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_hasUnreadPins: function() {
|
_hasUnreadPins: function() {
|
||||||
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
|
||||||
if (!currentPinEvent) return false;
|
if (!currentPinEvent) return false;
|
||||||
|
@ -409,6 +418,17 @@ module.exports = React.createClass({
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let toggleRightPanelButton;
|
||||||
|
if (this.props.isGrid) {
|
||||||
|
toggleRightPanelButton =
|
||||||
|
<AccessibleButton
|
||||||
|
className="mx_RoomHeader_button"
|
||||||
|
onClick={this.onToggleRightPanelClick}
|
||||||
|
title={_t('Toggle right panel')}>
|
||||||
|
<TintableSvg src="img/feather-icons/toggle-right-panel.svg" width="20" height="20" />
|
||||||
|
</AccessibleButton>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
|
<div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
|
||||||
<div className="mx_RoomHeader_wrapper">
|
<div className="mx_RoomHeader_wrapper">
|
||||||
|
@ -419,7 +439,8 @@ module.exports = React.createClass({
|
||||||
{ saveButton }
|
{ saveButton }
|
||||||
{ cancelButton }
|
{ cancelButton }
|
||||||
{ rightRow }
|
{ rightRow }
|
||||||
<RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} />
|
{ !this.props.isGrid ? <RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} /> : undefined }
|
||||||
|
{ toggleRightPanelButton }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -102,6 +102,7 @@ module.exports = React.createClass({
|
||||||
cli.on("Event.decrypted", this.onEventDecrypted);
|
cli.on("Event.decrypted", this.onEventDecrypted);
|
||||||
cli.on("accountData", this.onAccountData);
|
cli.on("accountData", this.onAccountData);
|
||||||
cli.on("Group.myMembership", this._onGroupMyMembership);
|
cli.on("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
cli.on("RoomState.events", this.onRoomStateEvents);
|
||||||
|
|
||||||
const dmRoomMap = DMRoomMap.shared();
|
const dmRoomMap = DMRoomMap.shared();
|
||||||
// A map between tags which are group IDs and the room IDs of rooms that should be kept
|
// A map between tags which are group IDs and the room IDs of rooms that should be kept
|
||||||
|
@ -230,6 +231,7 @@ module.exports = React.createClass({
|
||||||
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
|
MatrixClientPeg.get().removeListener("Event.decrypted", this.onEventDecrypted);
|
||||||
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
MatrixClientPeg.get().removeListener("accountData", this.onAccountData);
|
||||||
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
MatrixClientPeg.get().removeListener("Group.myMembership", this._onGroupMyMembership);
|
||||||
|
MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._tagStoreToken) {
|
if (this._tagStoreToken) {
|
||||||
|
@ -253,6 +255,12 @@ module.exports = React.createClass({
|
||||||
this.updateVisibleRooms();
|
this.updateVisibleRooms();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRoomStateEvents: function(ev, state) {
|
||||||
|
if (ev.getType() === "m.room.create" || ev.getType() === "m.room.tombstone") {
|
||||||
|
this.updateVisibleRooms();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
onDeleteRoom: function(roomId) {
|
onDeleteRoom: function(roomId) {
|
||||||
this.updateVisibleRooms();
|
this.updateVisibleRooms();
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2018 New Vector Ltd
|
Copyright 2018, 2019 New Vector Ltd
|
||||||
|
|
||||||
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.
|
||||||
|
@ -20,10 +20,16 @@ import sdk from "../../../index";
|
||||||
import { _t } from "../../../languageHandler";
|
import { _t } from "../../../languageHandler";
|
||||||
import Modal from "../../../Modal";
|
import Modal from "../../../Modal";
|
||||||
import MatrixClientPeg from "../../../MatrixClientPeg";
|
import MatrixClientPeg from "../../../MatrixClientPeg";
|
||||||
|
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
export default class RoomRecoveryReminder extends React.PureComponent {
|
export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
onFinished: PropTypes.func.isRequired,
|
// called if the user sets the option to suppress this reminder in the future
|
||||||
|
onDontAskAgainSet: PropTypes.func,
|
||||||
|
}
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
onDontAskAgainSet: function() {},
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -82,7 +88,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
|
||||||
userId: MatrixClientPeg.get().credentials.userId,
|
userId: MatrixClientPeg.get().credentials.userId,
|
||||||
device: this.state.unverifiedDevice,
|
device: this.state.unverifiedDevice,
|
||||||
onFinished: this.props.onFinished,
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -91,9 +96,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
// we'll show the create key backup flow.
|
// we'll show the create key backup flow.
|
||||||
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
|
||||||
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
import("../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog"),
|
||||||
{
|
|
||||||
onFinished: this.props.onFinished,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,10 +105,14 @@ export default class RoomRecoveryReminder extends React.PureComponent {
|
||||||
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
|
Modal.createTrackedDialogAsync("Ignore Recovery Reminder", "Ignore Recovery Reminder",
|
||||||
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
|
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
|
||||||
{
|
{
|
||||||
onDontAskAgain: () => {
|
onDontAskAgain: async () => {
|
||||||
// Report false to the caller, who should prevent the
|
await SettingsStore.setValue(
|
||||||
// reminder from appearing in the future.
|
"showRoomRecoveryReminder",
|
||||||
this.props.onFinished(false);
|
null,
|
||||||
|
SettingLevel.ACCOUNT,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
this.props.onDontAskAgainSet();
|
||||||
},
|
},
|
||||||
onSetup: () => {
|
onSetup: () => {
|
||||||
this.showSetupDialog();
|
this.showSetupDialog();
|
||||||
|
|
|
@ -29,7 +29,6 @@ import * as RoomNotifs from '../../../RoomNotifs';
|
||||||
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
import * as FormattingUtils from '../../../utils/FormattingUtils';
|
||||||
import AccessibleButton from '../elements/AccessibleButton';
|
import AccessibleButton from '../elements/AccessibleButton';
|
||||||
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
import ActiveRoomObserver from '../../../ActiveRoomObserver';
|
||||||
import RoomViewStore from '../../../stores/RoomViewStore';
|
|
||||||
import SettingsStore from "../../../settings/SettingsStore";
|
import SettingsStore from "../../../settings/SettingsStore";
|
||||||
|
|
||||||
module.exports = React.createClass({
|
module.exports = React.createClass({
|
||||||
|
@ -62,7 +61,7 @@ module.exports = React.createClass({
|
||||||
roomName: this.props.room.name,
|
roomName: this.props.room.name,
|
||||||
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
|
||||||
notificationCount: this.props.room.getUnreadNotificationCount(),
|
notificationCount: this.props.room.getUnreadNotificationCount(),
|
||||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
selected: this.props.room.roomId === ActiveRoomObserver.getActiveRoomId(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -117,9 +116,9 @@ module.exports = React.createClass({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
_onActiveRoomChange: function() {
|
_onActiveRoomChange: function(activeRoomId) {
|
||||||
this.setState({
|
this.setState({
|
||||||
selected: this.props.room.roomId === RoomViewStore.getRoomId(),
|
selected: this.props.room.roomId === activeRoomId,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -21,13 +21,15 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
|
||||||
import { _t } from '../../../languageHandler';
|
import { _t } from '../../../languageHandler';
|
||||||
import Modal from '../../../Modal';
|
import Modal from '../../../Modal';
|
||||||
|
|
||||||
export default class KeyBackupPanel extends React.Component {
|
export default class KeyBackupPanel extends React.PureComponent {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this._startNewBackup = this._startNewBackup.bind(this);
|
this._startNewBackup = this._startNewBackup.bind(this);
|
||||||
this._deleteBackup = this._deleteBackup.bind(this);
|
this._deleteBackup = this._deleteBackup.bind(this);
|
||||||
this._verifyDevice = this._verifyDevice.bind(this);
|
this._verifyDevice = this._verifyDevice.bind(this);
|
||||||
|
this._onKeyBackupSessionsRemaining =
|
||||||
|
this._onKeyBackupSessionsRemaining.bind(this);
|
||||||
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
|
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
|
||||||
this._restoreBackup = this._restoreBackup.bind(this);
|
this._restoreBackup = this._restoreBackup.bind(this);
|
||||||
|
|
||||||
|
@ -36,6 +38,7 @@ export default class KeyBackupPanel extends React.Component {
|
||||||
loading: true,
|
loading: true,
|
||||||
error: null,
|
error: null,
|
||||||
backupInfo: null,
|
backupInfo: null,
|
||||||
|
sessionsRemaining: 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +46,10 @@ export default class KeyBackupPanel extends React.Component {
|
||||||
this._loadBackupStatus();
|
this._loadBackupStatus();
|
||||||
|
|
||||||
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||||
|
MatrixClientPeg.get().on(
|
||||||
|
'crypto.keyBackupSessionsRemaining',
|
||||||
|
this._onKeyBackupSessionsRemaining,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -50,9 +57,19 @@ export default class KeyBackupPanel extends React.Component {
|
||||||
|
|
||||||
if (MatrixClientPeg.get()) {
|
if (MatrixClientPeg.get()) {
|
||||||
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
|
||||||
|
MatrixClientPeg.get().removeListener(
|
||||||
|
'crypto.keyBackupSessionsRemaining',
|
||||||
|
this._onKeyBackupSessionsRemaining,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_onKeyBackupSessionsRemaining(sessionsRemaining) {
|
||||||
|
this.setState({
|
||||||
|
sessionsRemaining,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
_onKeyBackupStatus() {
|
_onKeyBackupStatus() {
|
||||||
this._loadBackupStatus();
|
this._loadBackupStatus();
|
||||||
}
|
}
|
||||||
|
@ -144,57 +161,70 @@ export default class KeyBackupPanel extends React.Component {
|
||||||
} else if (this.state.backupInfo) {
|
} else if (this.state.backupInfo) {
|
||||||
let clientBackupStatus;
|
let clientBackupStatus;
|
||||||
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||||
clientBackupStatus = _t("This device is uploading keys to this backup");
|
clientBackupStatus = _t("This device is using key backup");
|
||||||
} else {
|
} else {
|
||||||
// XXX: display why and how to fix it
|
// XXX: display why and how to fix it
|
||||||
clientBackupStatus = _t(
|
clientBackupStatus = _t(
|
||||||
"This device is <b>not</b> uploading keys to this backup", {},
|
"This device is <b>not</b> using key backup", {},
|
||||||
{b: x => <b>{x}</b>},
|
{b: x => <b>{x}</b>},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let uploadStatus;
|
||||||
|
const { sessionsRemaining } = this.state;
|
||||||
|
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
|
||||||
|
// No upload status to show when backup disabled.
|
||||||
|
uploadStatus = "";
|
||||||
|
} else if (sessionsRemaining > 0) {
|
||||||
|
uploadStatus = <div>
|
||||||
|
{_t("Backing up %(sessionsRemaining)s keys...", { sessionsRemaining })} <br />
|
||||||
|
</div>;
|
||||||
|
} else {
|
||||||
|
uploadStatus = <div>
|
||||||
|
{_t("All keys backed up")} <br />
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
|
let backupSigStatuses = this.state.backupSigStatus.sigs.map((sig, i) => {
|
||||||
const deviceName = sig.device.getDisplayName() || sig.device.deviceId;
|
const deviceName = sig.device.getDisplayName() || sig.device.deviceId;
|
||||||
const sigStatusSubstitutions = {
|
const validity = sub =>
|
||||||
validity: sub =>
|
|
||||||
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
|
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
|
||||||
{sub}
|
{sub}
|
||||||
</span>,
|
</span>;
|
||||||
verify: sub =>
|
const verify = sub =>
|
||||||
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
|
||||||
{sub}
|
{sub}
|
||||||
</span>,
|
</span>;
|
||||||
device: sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>,
|
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
|
||||||
};
|
|
||||||
let sigStatus;
|
let sigStatus;
|
||||||
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
|
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has a <validity>valid</validity> signature from this device",
|
"Backup has a <validity>valid</validity> signature from this device",
|
||||||
{}, sigStatusSubstitutions,
|
{}, { validity },
|
||||||
);
|
);
|
||||||
} else if (sig.valid && sig.device.isVerified()) {
|
} else if (sig.valid && sig.device.isVerified()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has a <validity>valid</validity> signature from " +
|
"Backup has a <validity>valid</validity> signature from " +
|
||||||
"<verify>verified</verify> device <device></device>",
|
"<verify>verified</verify> device <device></device>",
|
||||||
{}, sigStatusSubstitutions,
|
{}, { validity, verify, device },
|
||||||
);
|
);
|
||||||
} else if (sig.valid && !sig.device.isVerified()) {
|
} else if (sig.valid && !sig.device.isVerified()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has a <validity>valid</validity> signature from " +
|
"Backup has a <validity>valid</validity> signature from " +
|
||||||
"<verify>unverified</verify> device <device></device>",
|
"<verify>unverified</verify> device <device></device>",
|
||||||
{}, sigStatusSubstitutions,
|
{}, { validity, verify, device },
|
||||||
);
|
);
|
||||||
} else if (!sig.valid && sig.device.isVerified()) {
|
} else if (!sig.valid && sig.device.isVerified()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has an <validity>invalid</validity> signature from " +
|
"Backup has an <validity>invalid</validity> signature from " +
|
||||||
"<verify>verified</verify> device <device></device>",
|
"<verify>verified</verify> device <device></device>",
|
||||||
{}, sigStatusSubstitutions,
|
{}, { validity, verify, device },
|
||||||
);
|
);
|
||||||
} else if (!sig.valid && !sig.device.isVerified()) {
|
} else if (!sig.valid && !sig.device.isVerified()) {
|
||||||
sigStatus = _t(
|
sigStatus = _t(
|
||||||
"Backup has an <validity>invalid</validity> signature from " +
|
"Backup has an <validity>invalid</validity> signature from " +
|
||||||
"<verify>unverified</verify> device <device></device>",
|
"<verify>unverified</verify> device <device></device>",
|
||||||
{}, sigStatusSubstitutions,
|
{}, { validity, verify, device },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +249,7 @@ export default class KeyBackupPanel extends React.Component {
|
||||||
{_t("Backup version: ")}{this.state.backupInfo.version}<br />
|
{_t("Backup version: ")}{this.state.backupInfo.version}<br />
|
||||||
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
|
{_t("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
|
||||||
{clientBackupStatus}<br />
|
{clientBackupStatus}<br />
|
||||||
|
{uploadStatus}
|
||||||
<div>{backupSigStatuses}</div><br />
|
<div>{backupSigStatuses}</div><br />
|
||||||
<br />
|
<br />
|
||||||
<AccessibleButton className="mx_UserSettings_button"
|
<AccessibleButton className="mx_UserSettings_button"
|
||||||
|
|
|
@ -17,42 +17,10 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const flux = require("flux");
|
import MatrixDispatcher from "./matrix-dispatcher";
|
||||||
|
|
||||||
class MatrixDispatcher extends flux.Dispatcher {
|
|
||||||
/**
|
|
||||||
* @param {Object|function} payload Required. The payload to dispatch.
|
|
||||||
* If an Object, must contain at least an 'action' key.
|
|
||||||
* If a function, must have the signature (dispatch) => {...}.
|
|
||||||
* @param {boolean=} sync Optional. Pass true to dispatch
|
|
||||||
* synchronously. This is useful for anything triggering
|
|
||||||
* an operation that the browser requires user interaction
|
|
||||||
* for.
|
|
||||||
*/
|
|
||||||
dispatch(payload, sync) {
|
|
||||||
// Allow for asynchronous dispatching by accepting payloads that have the
|
|
||||||
// type `function (dispatch) {...}`
|
|
||||||
if (typeof payload === 'function') {
|
|
||||||
payload((action) => {
|
|
||||||
this.dispatch(action, sync);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sync) {
|
|
||||||
super.dispatch(payload);
|
|
||||||
} else {
|
|
||||||
// Unless the caller explicitly asked for us to dispatch synchronously,
|
|
||||||
// we always set a timeout to do this: The flux dispatcher complains
|
|
||||||
// if you dispatch from within a dispatch, so rather than action
|
|
||||||
// handlers having to worry about not calling anything that might
|
|
||||||
// then dispatch, we just do dispatches asynchronously.
|
|
||||||
setTimeout(super.dispatch.bind(this, payload), 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.mxDispatcher === undefined) {
|
if (global.mxDispatcher === undefined) {
|
||||||
global.mxDispatcher = new MatrixDispatcher();
|
global.mxDispatcher = new MatrixDispatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = global.mxDispatcher;
|
module.exports = global.mxDispatcher;
|
||||||
|
|
|
@ -183,6 +183,7 @@
|
||||||
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
"%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".",
|
||||||
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
"%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.",
|
||||||
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.",
|
"%(senderDisplayName)s changed the room name to %(roomName)s.": "%(senderDisplayName)s changed the room name to %(roomName)s.",
|
||||||
|
"%(senderDisplayName)s upgraded this room.": "%(senderDisplayName)s upgraded this room.",
|
||||||
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
"%(senderDisplayName)s sent an image.": "%(senderDisplayName)s sent an image.",
|
||||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
|
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|other": "%(senderName)s added %(addedAddresses)s as addresses for this room.",
|
||||||
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
|
"%(senderName)s added %(count)s %(addedAddresses)s as addresses for this room.|one": "%(senderName)s added %(addedAddresses)s as an address for this room.",
|
||||||
|
@ -223,8 +224,10 @@
|
||||||
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
"Your browser does not support the required cryptography extensions": "Your browser does not support the required cryptography extensions",
|
||||||
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
"Not a valid Riot keyfile": "Not a valid Riot keyfile",
|
||||||
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
"Authentication check failed: incorrect password?": "Authentication check failed: incorrect password?",
|
||||||
|
"Unrecognised address": "Unrecognised address",
|
||||||
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
|
"You do not have permission to invite people to this room.": "You do not have permission to invite people to this room.",
|
||||||
"User %(user_id)s does not exist": "User %(user_id)s does not exist",
|
"User %(user_id)s does not exist": "User %(user_id)s does not exist",
|
||||||
|
"User %(user_id)s may or may not exist": "User %(user_id)s may or may not exist",
|
||||||
"Unknown server error": "Unknown server error",
|
"Unknown server error": "Unknown server error",
|
||||||
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
"Use a few words, avoid common phrases": "Use a few words, avoid common phrases",
|
||||||
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",
|
"No need for symbols, digits, or uppercase letters": "No need for symbols, digits, or uppercase letters",
|
||||||
|
@ -261,6 +264,8 @@
|
||||||
"Custom user status messages": "Custom user status messages",
|
"Custom user status messages": "Custom user status messages",
|
||||||
"Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view",
|
"Increase performance by only loading room members on first view": "Increase performance by only loading room members on first view",
|
||||||
"Backup of encryption keys to server": "Backup of encryption keys to server",
|
"Backup of encryption keys to server": "Backup of encryption keys to server",
|
||||||
|
"Render simple counters in room header": "Render simple counters in room header",
|
||||||
|
"Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu": "Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu",
|
||||||
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
|
"Disable Emoji suggestions while typing": "Disable Emoji suggestions while typing",
|
||||||
"Use compact timeline layout": "Use compact timeline layout",
|
"Use compact timeline layout": "Use compact timeline layout",
|
||||||
"Hide removed messages": "Hide removed messages",
|
"Hide removed messages": "Hide removed messages",
|
||||||
|
@ -292,6 +297,7 @@
|
||||||
"Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list",
|
"Pin unread rooms to the top of the room list": "Pin unread rooms to the top of the room list",
|
||||||
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
|
"Enable widget screenshots on supported widgets": "Enable widget screenshots on supported widgets",
|
||||||
"Show empty room list headings": "Show empty room list headings",
|
"Show empty room list headings": "Show empty room list headings",
|
||||||
|
"Always invite users which may not exist": "Always invite users which may not exist",
|
||||||
"Show developer tools": "Show developer tools",
|
"Show developer tools": "Show developer tools",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
|
@ -352,8 +358,10 @@
|
||||||
"Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history",
|
"Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history": "Delete your backed up encryption keys from the server? You will no longer be able to use your recovery key to read encrypted message history",
|
||||||
"Delete backup": "Delete backup",
|
"Delete backup": "Delete backup",
|
||||||
"Unable to load key backup status": "Unable to load key backup status",
|
"Unable to load key backup status": "Unable to load key backup status",
|
||||||
"This device is uploading keys to this backup": "This device is uploading keys to this backup",
|
"This device is using key backup": "This device is using key backup",
|
||||||
"This device is <b>not</b> uploading keys to this backup": "This device is <b>not</b> uploading keys to this backup",
|
"This device is <b>not</b> using key backup": "This device is <b>not</b> using key backup",
|
||||||
|
"Backing up %(sessionsRemaining)s keys...": "Backing up %(sessionsRemaining)s keys...",
|
||||||
|
"All keys backed up": "All keys backed up",
|
||||||
"Backup has a <validity>valid</validity> signature from this device": "Backup has a <validity>valid</validity> signature from this device",
|
"Backup has a <validity>valid</validity> signature from this device": "Backup has a <validity>valid</validity> signature from this device",
|
||||||
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>",
|
"Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>verified</verify> device <device></device>",
|
||||||
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>",
|
"Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>": "Backup has a <validity>valid</validity> signature from <verify>unverified</verify> device <device></device>",
|
||||||
|
@ -535,6 +543,7 @@
|
||||||
"Forget room": "Forget room",
|
"Forget room": "Forget room",
|
||||||
"Search": "Search",
|
"Search": "Search",
|
||||||
"Share room": "Share room",
|
"Share room": "Share room",
|
||||||
|
"Toggle right panel": "Toggle right panel",
|
||||||
"Drop here to favourite": "Drop here to favourite",
|
"Drop here to favourite": "Drop here to favourite",
|
||||||
"Drop here to tag direct chat": "Drop here to tag direct chat",
|
"Drop here to tag direct chat": "Drop here to tag direct chat",
|
||||||
"Drop here to restore": "Drop here to restore",
|
"Drop here to restore": "Drop here to restore",
|
||||||
|
@ -883,6 +892,10 @@
|
||||||
"That doesn't look like a valid email address": "That doesn't look like a valid email address",
|
"That doesn't look like a valid email address": "That doesn't look like a valid email address",
|
||||||
"You have entered an invalid address.": "You have entered an invalid address.",
|
"You have entered an invalid address.": "You have entered an invalid address.",
|
||||||
"Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.",
|
"Try using one of the following valid address types: %(validTypesList)s.": "Try using one of the following valid address types: %(validTypesList)s.",
|
||||||
|
"The following users may not exist": "The following users may not exist",
|
||||||
|
"The following users may not exist - would you like to invite them anyways?": "The following users may not exist - would you like to invite them anyways?",
|
||||||
|
"Invite anyways and never warn me again": "Invite anyways and never warn me again",
|
||||||
|
"Invite anyways": "Invite anyways",
|
||||||
"Preparing to send logs": "Preparing to send logs",
|
"Preparing to send logs": "Preparing to send logs",
|
||||||
"Logs sent": "Logs sent",
|
"Logs sent": "Logs sent",
|
||||||
"Thank you!": "Thank you!",
|
"Thank you!": "Thank you!",
|
||||||
|
@ -1077,6 +1090,7 @@
|
||||||
"Direct Chat": "Direct Chat",
|
"Direct Chat": "Direct Chat",
|
||||||
"Set a new status...": "Set a new status...",
|
"Set a new status...": "Set a new status...",
|
||||||
"Clear status": "Clear status",
|
"Clear status": "Clear status",
|
||||||
|
"View as Grid": "View as Grid",
|
||||||
"View Community": "View Community",
|
"View Community": "View Community",
|
||||||
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",
|
"Sorry, your browser is <b>not</b> able to run Riot.": "Sorry, your browser is <b>not</b> able to run Riot.",
|
||||||
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
|
"Riot uses many advanced browser features, some of which are not available or experimental in your current browser.": "Riot uses many advanced browser features, some of which are not available or experimental in your current browser.",
|
||||||
|
@ -1087,6 +1101,7 @@
|
||||||
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
"You must <a>register</a> to use this functionality": "You must <a>register</a> to use this functionality",
|
||||||
"You must join the room to see its files": "You must join the room to see its files",
|
"You must join the room to see its files": "You must join the room to see its files",
|
||||||
"There are no visible files in this room": "There are no visible files in this room",
|
"There are no visible files in this room": "There are no visible files in this room",
|
||||||
|
"No room in this tile yet.": "No room in this tile yet.",
|
||||||
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n",
|
"<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n": "<h1>HTML for your community's page</h1>\n<p>\n Use the long description to introduce new members to the community, or distribute\n some important <a href=\"foo\">links</a>\n</p>\n<p>\n You can even use 'img' tags\n</p>\n",
|
||||||
"Add rooms to the community summary": "Add rooms to the community summary",
|
"Add rooms to the community summary": "Add rooms to the community summary",
|
||||||
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
|
"Which rooms would you like to add to this summary?": "Which rooms would you like to add to this summary?",
|
||||||
|
@ -1386,26 +1401,31 @@
|
||||||
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
|
"<b>Print it</b> and store it somewhere safe": "<b>Print it</b> and store it somewhere safe",
|
||||||
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
|
"<b>Save it</b> on a USB key or backup drive": "<b>Save it</b> on a USB key or backup drive",
|
||||||
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
|
"<b>Copy it</b> to your personal cloud storage": "<b>Copy it</b> to your personal cloud storage",
|
||||||
"Backup created": "Backup created",
|
"Your encryption keys are now being backed up in the background to your Homeserver. The initial backup could take several minutes. You can view key backup upload progress in Settings.": "Your encryption keys are now being backed up in the background to your Homeserver. The initial backup could take several minutes. You can view key backup upload progress in Settings.",
|
||||||
"Your encryption keys are now being backed up to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.",
|
|
||||||
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.",
|
"Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.": "Without setting up Secure Message Recovery, you won't be able to restore your encrypted message history if you log out or use another device.",
|
||||||
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
|
"Set up Secure Message Recovery": "Set up Secure Message Recovery",
|
||||||
"Create a Recovery Passphrase": "Create a Recovery Passphrase",
|
"Create a Recovery Passphrase": "Create a Recovery Passphrase",
|
||||||
"Confirm Recovery Passphrase": "Confirm Recovery Passphrase",
|
"Confirm Recovery Passphrase": "Confirm Recovery Passphrase",
|
||||||
"Recovery Key": "Recovery Key",
|
"Recovery Key": "Recovery Key",
|
||||||
"Keep it safe": "Keep it safe",
|
"Keep it safe": "Keep it safe",
|
||||||
"Backing up...": "Backing up...",
|
"Starting backup...": "Starting backup...",
|
||||||
|
"Backup Started": "Backup Started",
|
||||||
"Create Key Backup": "Create Key Backup",
|
"Create Key Backup": "Create Key Backup",
|
||||||
"Unable to create key backup": "Unable to create key backup",
|
"Unable to create key backup": "Unable to create key backup",
|
||||||
"Retry": "Retry",
|
"Retry": "Retry",
|
||||||
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
|
"Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.": "Without setting up Secure Message Recovery, you'll lose your secure message history when you log out.",
|
||||||
"If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
|
"If you don't want to set this up now, you can later in Settings.": "If you don't want to set this up now, you can later in Settings.",
|
||||||
"New Recovery Method": "New Recovery Method",
|
"New Recovery Method": "New Recovery Method",
|
||||||
"A new recovery passphrase and key for Secure Messages has been detected.": "A new recovery passphrase and key for Secure Messages has been detected.",
|
"A new recovery passphrase and key for Secure Messages have been detected.": "A new recovery passphrase and key for Secure Messages have been detected.",
|
||||||
"Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.",
|
|
||||||
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
|
"If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't set the new recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
|
||||||
"Set up Secure Messages": "Set up Secure Messages",
|
"This device is encrypting history using the new recovery method.": "This device is encrypting history using the new recovery method.",
|
||||||
"Go to Settings": "Go to Settings",
|
"Go to Settings": "Go to Settings",
|
||||||
|
"Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.": "Setting up Secure Messages on this device will re-encrypt this device's message history with the new recovery method.",
|
||||||
|
"Set up Secure Messages": "Set up Secure Messages",
|
||||||
|
"Recovery Method Removed": "Recovery Method Removed",
|
||||||
|
"This device has detected that your recovery passphrase and key for Secure Messages have been removed.": "This device has detected that your recovery passphrase and key for Secure Messages have been removed.",
|
||||||
|
"If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.": "If you did this accidentally, you can setup Secure Messages on this device which will re-encrypt this device's message history with a new recovery method.",
|
||||||
|
"If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.": "If you didn't remove the recovery method, an attacker may be trying to access your account. Change your account password and set a new recovery method immediately in Settings.",
|
||||||
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
"Failed to set direct chat tag": "Failed to set direct chat tag",
|
||||||
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
"Failed to remove tag %(tagName)s from room": "Failed to remove tag %(tagName)s from room",
|
||||||
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
"Failed to add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room"
|
||||||
|
|
53
src/matrix-dispatcher.js
Normal file
53
src/matrix-dispatcher.js
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
Copyright 2015, 2016 OpenMarket Ltd
|
||||||
|
Copyright 2017 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const flux = require("flux");
|
||||||
|
|
||||||
|
export default class MatrixDispatcher extends flux.Dispatcher {
|
||||||
|
/**
|
||||||
|
* @param {Object|function} payload Required. The payload to dispatch.
|
||||||
|
* If an Object, must contain at least an 'action' key.
|
||||||
|
* If a function, must have the signature (dispatch) => {...}.
|
||||||
|
* @param {boolean=} sync Optional. Pass true to dispatch
|
||||||
|
* synchronously. This is useful for anything triggering
|
||||||
|
* an operation that the browser requires user interaction
|
||||||
|
* for.
|
||||||
|
*/
|
||||||
|
dispatch(payload, sync) {
|
||||||
|
// Allow for asynchronous dispatching by accepting payloads that have the
|
||||||
|
// type `function (dispatch) {...}`
|
||||||
|
if (typeof payload === 'function') {
|
||||||
|
payload((action) => {
|
||||||
|
this.dispatch(action, sync);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
super.dispatch(payload);
|
||||||
|
} else {
|
||||||
|
// Unless the caller explicitly asked for us to dispatch synchronously,
|
||||||
|
// we always set a timeout to do this: The flux dispatcher complains
|
||||||
|
// if you dispatch from within a dispatch, so rather than action
|
||||||
|
// handlers having to worry about not calling anything that might
|
||||||
|
// then dispatch, we just do dispatches asynchronously.
|
||||||
|
setTimeout(super.dispatch.bind(this, payload), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -102,6 +102,18 @@ export const SETTINGS = {
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"feature_state_counters": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Render simple counters in room header"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
"feature_gridview": {
|
||||||
|
isFeature: true,
|
||||||
|
displayName: _td("Allow up to 6 rooms in a community to be shown simultaneously in a grid via the context menu"),
|
||||||
|
supportedLevels: LEVELS_FEATURE,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"MessageComposerInput.dontSuggestEmoji": {
|
"MessageComposerInput.dontSuggestEmoji": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Disable Emoji suggestions while typing'),
|
displayName: _td('Disable Emoji suggestions while typing'),
|
||||||
|
@ -317,6 +329,11 @@ export const SETTINGS = {
|
||||||
displayName: _td('Show empty room list headings'),
|
displayName: _td('Show empty room list headings'),
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
|
"alwaysInviteUnknownUsers": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
displayName: _td('Always invite users which may not exist'),
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
"showDeveloperTools": {
|
"showDeveloperTools": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td('Show developer tools'),
|
displayName: _td('Show developer tools'),
|
||||||
|
|
277
src/stores/OpenRoomsStore.js
Normal file
277
src/stores/OpenRoomsStore.js
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
/*
|
||||||
|
Copyright 2018 New Vector Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
import MatrixDispatcher from '../matrix-dispatcher';
|
||||||
|
import dis from '../dispatcher';
|
||||||
|
import {RoomViewStore} from './RoomViewStore';
|
||||||
|
import GroupStore from './GroupStore';
|
||||||
|
import {Store} from 'flux/utils';
|
||||||
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
|
|
||||||
|
|
||||||
|
function matchesRoom(payload, roomStore) {
|
||||||
|
if (!roomStore) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (payload.room_alias) {
|
||||||
|
return payload.room_alias === roomStore.getRoomAlias();
|
||||||
|
}
|
||||||
|
return payload.room_id === roomStore.getRoomId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A class for keeping track of the RoomViewStores of the rooms shown on the screen.
|
||||||
|
* Routes the dispatcher actions to the store of currently active room.
|
||||||
|
*/
|
||||||
|
class OpenRoomsStore extends Store {
|
||||||
|
constructor() {
|
||||||
|
super(dis);
|
||||||
|
|
||||||
|
// Initialise state
|
||||||
|
this._state = {
|
||||||
|
rooms: [],
|
||||||
|
currentIndex: null,
|
||||||
|
group_id: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._forwardingEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomStores() {
|
||||||
|
return this._state.rooms.map((r) => r.store);
|
||||||
|
}
|
||||||
|
|
||||||
|
getActiveRoomStore() {
|
||||||
|
const openRoom = this._getActiveOpenRoom();
|
||||||
|
if (openRoom) {
|
||||||
|
return openRoom.store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRoomStoreAt(index) {
|
||||||
|
if (index >= 0 && index < this._state.rooms.length) {
|
||||||
|
return this._state.rooms[index].store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getActiveOpenRoom() {
|
||||||
|
const index = this._state.currentIndex;
|
||||||
|
if (index !== null && index < this._state.rooms.length) {
|
||||||
|
return this._state.rooms[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setState(newState) {
|
||||||
|
this._state = Object.assign(this._state, newState);
|
||||||
|
this.__emitChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasRoom(payload) {
|
||||||
|
return this._roomIndex(payload) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_roomIndex(payload) {
|
||||||
|
return this._state.rooms.findIndex((r) => matchesRoom(payload, r.store));
|
||||||
|
}
|
||||||
|
|
||||||
|
_cleanupOpenRooms() {
|
||||||
|
this._state.rooms.forEach((room) => {
|
||||||
|
room.dispatcher.unregister(room.dispatcherRef);
|
||||||
|
room.dispatcher.unregister(room.store.getDispatchToken());
|
||||||
|
});
|
||||||
|
this._setState({
|
||||||
|
rooms: [],
|
||||||
|
group_id: null,
|
||||||
|
currentIndex: null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_createOpenRoom(roomId, roomAlias) {
|
||||||
|
const dispatcher = new MatrixDispatcher();
|
||||||
|
// forward all actions coming from the room dispatcher
|
||||||
|
// to the global one
|
||||||
|
const dispatcherRef = dispatcher.register((payload) => {
|
||||||
|
// block a view_room action for the same room because it will switch to
|
||||||
|
// single room mode in MatrixChat
|
||||||
|
if (payload.action === 'view_room' && roomId === payload.room_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
payload.grid_src_room_id = roomId;
|
||||||
|
payload.grid_src_room_alias = roomAlias;
|
||||||
|
this.getDispatcher().dispatch(payload);
|
||||||
|
});
|
||||||
|
const openRoom = {
|
||||||
|
store: new RoomViewStore(dispatcher),
|
||||||
|
dispatcher,
|
||||||
|
dispatcherRef,
|
||||||
|
};
|
||||||
|
|
||||||
|
dispatcher.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: roomId,
|
||||||
|
room_alias: roomAlias,
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
return openRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
_setSingleOpenRoom(payload) {
|
||||||
|
this._setState({
|
||||||
|
rooms: [this._createOpenRoom(payload.room_id, payload.room_alias)],
|
||||||
|
currentIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_setGroupOpenRooms(groupId) {
|
||||||
|
this._cleanupOpenRooms();
|
||||||
|
// TODO: register to GroupStore updates
|
||||||
|
const rooms = GroupStore.getGroupRooms(groupId);
|
||||||
|
const openRooms = rooms.map((room) => {
|
||||||
|
return this._createOpenRoom(room.roomId);
|
||||||
|
});
|
||||||
|
this._setState({
|
||||||
|
rooms: openRooms,
|
||||||
|
group_id: groupId,
|
||||||
|
currentIndex: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_forwardAction(payload) {
|
||||||
|
// don't forward an event to a room dispatcher
|
||||||
|
// if the event originated from that dispatcher, as this
|
||||||
|
// would cause the event to be observed twice in that
|
||||||
|
// dispatcher
|
||||||
|
if (payload.grid_src_room_id || payload.grid_src_room_alias) {
|
||||||
|
const srcPayload = {
|
||||||
|
room_id: payload.grid_src_room_id,
|
||||||
|
room_alias: payload.grid_src_room_alias,
|
||||||
|
};
|
||||||
|
const srcIndex = this._roomIndex(srcPayload);
|
||||||
|
if (srcIndex === this._state.currentIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const currentRoom = this._getActiveOpenRoom();
|
||||||
|
if (currentRoom) {
|
||||||
|
currentRoom.dispatcher.dispatch(payload, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async _resolveRoomAlias(payload) {
|
||||||
|
try {
|
||||||
|
const result = await MatrixClientPeg.get()
|
||||||
|
.getRoomIdForAlias(payload.room_alias);
|
||||||
|
this.getDispatcher().dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: result.room_id,
|
||||||
|
event_id: payload.event_id,
|
||||||
|
highlighted: payload.highlighted,
|
||||||
|
room_alias: payload.room_alias,
|
||||||
|
auto_join: payload.auto_join,
|
||||||
|
oob_data: payload.oob_data,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
this._forwardAction({
|
||||||
|
action: 'view_room_error',
|
||||||
|
room_id: null,
|
||||||
|
room_alias: payload.room_alias,
|
||||||
|
err: err,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_viewRoom(payload) {
|
||||||
|
console.log("!!! OpenRoomsStore: view_room", payload);
|
||||||
|
if (!payload.room_id && payload.room_alias) {
|
||||||
|
this._resolveRoomAlias(payload);
|
||||||
|
}
|
||||||
|
const currentStore = this.getActiveRoomStore();
|
||||||
|
if (!matchesRoom(payload, currentStore)) {
|
||||||
|
if (this._hasRoom(payload)) {
|
||||||
|
const roomIndex = this._roomIndex(payload);
|
||||||
|
this._setState({currentIndex: roomIndex});
|
||||||
|
} else {
|
||||||
|
this._cleanupOpenRooms();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!this.getActiveRoomStore()) {
|
||||||
|
console.log("OpenRoomsStore: _setSingleOpenRoom");
|
||||||
|
this._setSingleOpenRoom(payload);
|
||||||
|
}
|
||||||
|
console.log("OpenRoomsStore: _forwardAction");
|
||||||
|
this._forwardAction(payload);
|
||||||
|
if (this._forwardingEvent) {
|
||||||
|
this.getDispatcher().dispatch({
|
||||||
|
action: 'send_event',
|
||||||
|
room_id: payload.room_id,
|
||||||
|
event: this._forwardingEvent,
|
||||||
|
});
|
||||||
|
this._forwardingEvent = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__onDispatch(payload) {
|
||||||
|
let proposedIndex;
|
||||||
|
switch (payload.action) {
|
||||||
|
// view_room:
|
||||||
|
// - room_alias: '#somealias:matrix.org'
|
||||||
|
// - room_id: '!roomid123:matrix.org'
|
||||||
|
// - event_id: '$213456782:matrix.org'
|
||||||
|
// - event_offset: 100
|
||||||
|
// - highlighted: true
|
||||||
|
case 'view_room':
|
||||||
|
this._viewRoom(payload);
|
||||||
|
break;
|
||||||
|
case 'view_my_groups':
|
||||||
|
case 'view_group':
|
||||||
|
this._forwardAction(payload);
|
||||||
|
this._cleanupOpenRooms();
|
||||||
|
break;
|
||||||
|
case 'will_join':
|
||||||
|
case 'cancel_join':
|
||||||
|
case 'join_room':
|
||||||
|
case 'join_room_error':
|
||||||
|
case 'on_logged_out':
|
||||||
|
case 'reply_to_event':
|
||||||
|
case 'open_room_settings':
|
||||||
|
case 'close_settings':
|
||||||
|
case 'focus_composer':
|
||||||
|
this._forwardAction(payload);
|
||||||
|
break;
|
||||||
|
case 'forward_event':
|
||||||
|
this._forwardingEvent = payload.event;
|
||||||
|
break;
|
||||||
|
case 'group_grid_set_active':
|
||||||
|
proposedIndex = this._roomIndex(payload);
|
||||||
|
if (proposedIndex !== -1) {
|
||||||
|
this._setState({
|
||||||
|
currentIndex: proposedIndex,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'group_grid_view':
|
||||||
|
if (payload.group_id !== this._state.group_id) {
|
||||||
|
this._setGroupOpenRooms(payload.group_id);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let singletonOpenRoomsStore = null;
|
||||||
|
if (!singletonOpenRoomsStore) {
|
||||||
|
singletonOpenRoomsStore = new OpenRoomsStore();
|
||||||
|
}
|
||||||
|
module.exports = singletonOpenRoomsStore;
|
|
@ -14,7 +14,6 @@ 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 dis from '../dispatcher';
|
|
||||||
import {Store} from 'flux/utils';
|
import {Store} from 'flux/utils';
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import sdk from '../index';
|
import sdk from '../index';
|
||||||
|
@ -53,12 +52,12 @@ const INITIAL_STATE = {
|
||||||
* with a subset of the js-sdk.
|
* with a subset of the js-sdk.
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
class RoomViewStore extends Store {
|
export class RoomViewStore extends Store {
|
||||||
constructor() {
|
constructor(dispatcher) {
|
||||||
super(dis);
|
super(dispatcher);
|
||||||
|
|
||||||
// Initialise state
|
// Initialise state
|
||||||
this._state = INITIAL_STATE;
|
this._state = Object.assign({}, INITIAL_STATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
_setState(newState) {
|
_setState(newState) {
|
||||||
|
@ -85,6 +84,8 @@ class RoomViewStore extends Store {
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case 'view_room_error':
|
case 'view_room_error':
|
||||||
|
// should not go over dispatcher anymore
|
||||||
|
// but be internal to RoomViewStore
|
||||||
this._viewRoomError(payload);
|
this._viewRoomError(payload);
|
||||||
break;
|
break;
|
||||||
case 'will_join':
|
case 'will_join':
|
||||||
|
@ -150,22 +151,11 @@ class RoomViewStore extends Store {
|
||||||
// pull the user out of Room Settings
|
// pull the user out of Room Settings
|
||||||
isEditingSettings: false,
|
isEditingSettings: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this._state.forwardingEvent) {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'send_event',
|
|
||||||
room_id: newState.roomId,
|
|
||||||
event: this._state.forwardingEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this._setState(newState);
|
this._setState(newState);
|
||||||
|
|
||||||
if (payload.auto_join) {
|
if (payload.auto_join) {
|
||||||
this._joinRoom(payload);
|
this._joinRoom(payload);
|
||||||
}
|
}
|
||||||
} else if (payload.room_alias) {
|
} else if (payload.room_alias) {
|
||||||
// Resolve the alias and then do a second dispatch with the room ID acquired
|
|
||||||
this._setState({
|
this._setState({
|
||||||
roomId: null,
|
roomId: null,
|
||||||
initialEventId: null,
|
initialEventId: null,
|
||||||
|
@ -175,25 +165,6 @@ class RoomViewStore extends Store {
|
||||||
roomLoading: true,
|
roomLoading: true,
|
||||||
roomLoadError: null,
|
roomLoadError: null,
|
||||||
});
|
});
|
||||||
MatrixClientPeg.get().getRoomIdForAlias(payload.room_alias).done(
|
|
||||||
(result) => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room',
|
|
||||||
room_id: result.room_id,
|
|
||||||
event_id: payload.event_id,
|
|
||||||
highlighted: payload.highlighted,
|
|
||||||
room_alias: payload.room_alias,
|
|
||||||
auto_join: payload.auto_join,
|
|
||||||
oob_data: payload.oob_data,
|
|
||||||
});
|
|
||||||
}, (err) => {
|
|
||||||
dis.dispatch({
|
|
||||||
action: 'view_room_error',
|
|
||||||
room_id: null,
|
|
||||||
room_alias: payload.room_alias,
|
|
||||||
err: err,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,7 +190,7 @@ class RoomViewStore extends Store {
|
||||||
// stream yet, and that's the point at which we'd consider
|
// stream yet, and that's the point at which we'd consider
|
||||||
// the user joined to the room.
|
// the user joined to the room.
|
||||||
}, (err) => {
|
}, (err) => {
|
||||||
dis.dispatch({
|
this.getDispatcher().dispatch({
|
||||||
action: 'join_room_error',
|
action: 'join_room_error',
|
||||||
err: err,
|
err: err,
|
||||||
});
|
});
|
||||||
|
@ -335,8 +306,7 @@ class RoomViewStore extends Store {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let singletonRoomViewStore = null;
|
const MatrixDispatcher = require("../matrix-dispatcher");
|
||||||
if (!singletonRoomViewStore) {
|
const backwardsCompatInstance = new RoomViewStore(new MatrixDispatcher());
|
||||||
singletonRoomViewStore = new RoomViewStore();
|
|
||||||
}
|
export default backwardsCompatInstance;
|
||||||
module.exports = singletonRoomViewStore;
|
|
||||||
|
|
|
@ -15,11 +15,15 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
import MatrixClientPeg from '../MatrixClientPeg';
|
import MatrixClientPeg from '../MatrixClientPeg';
|
||||||
import {getAddressType} from '../UserAddress';
|
import {getAddressType} from '../UserAddress';
|
||||||
import GroupStore from '../stores/GroupStore';
|
import GroupStore from '../stores/GroupStore';
|
||||||
import Promise from 'bluebird';
|
import Promise from 'bluebird';
|
||||||
import {_t} from "../languageHandler";
|
import {_t} from "../languageHandler";
|
||||||
|
import sdk from "../index";
|
||||||
|
import Modal from "../Modal";
|
||||||
|
import SettingsStore from "../settings/SettingsStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
* Invites multiple addresses to a room or group, handling rate limiting from the server
|
||||||
|
@ -41,7 +45,7 @@ export default class MultiInviter {
|
||||||
this.addrs = [];
|
this.addrs = [];
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
this.completionStates = {}; // State of each address (invited or error)
|
this.completionStates = {}; // State of each address (invited or error)
|
||||||
this.errorTexts = {}; // Textual error per address
|
this.errors = {}; // { address: {errorText, errcode} }
|
||||||
this.deferred = null;
|
this.deferred = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +65,10 @@ export default class MultiInviter {
|
||||||
for (const addr of this.addrs) {
|
for (const addr of this.addrs) {
|
||||||
if (getAddressType(addr) === null) {
|
if (getAddressType(addr) === null) {
|
||||||
this.completionStates[addr] = 'error';
|
this.completionStates[addr] = 'error';
|
||||||
this.errorTexts[addr] = 'Unrecognised address';
|
this.errors[addr] = {
|
||||||
|
errcode: 'M_INVALID',
|
||||||
|
errorText: _t('Unrecognised address'),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.deferred = Promise.defer();
|
this.deferred = Promise.defer();
|
||||||
|
@ -85,18 +92,28 @@ export default class MultiInviter {
|
||||||
}
|
}
|
||||||
|
|
||||||
getErrorText(addr) {
|
getErrorText(addr) {
|
||||||
return this.errorTexts[addr];
|
return this.errors[addr] ? this.errors[addr].errorText : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async _inviteToRoom(roomId, addr) {
|
async _inviteToRoom(roomId, addr, ignoreProfile) {
|
||||||
const addrType = getAddressType(addr);
|
const addrType = getAddressType(addr);
|
||||||
|
|
||||||
if (addrType === 'email') {
|
if (addrType === 'email') {
|
||||||
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
return MatrixClientPeg.get().inviteByEmail(roomId, addr);
|
||||||
} else if (addrType === 'mx-user-id') {
|
} else if (addrType === 'mx-user-id') {
|
||||||
|
if (!ignoreProfile && !SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) {
|
||||||
|
try {
|
||||||
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
|
const profile = await MatrixClientPeg.get().getProfileInfo(addr);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return Promise.reject({errcode: "M_NOT_FOUND", error: "User does not have a profile."});
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
throw new Error("User has no profile");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw {
|
||||||
|
errcode: "RIOT.USER_NOT_FOUND",
|
||||||
|
error: "User does not have a profile or does not exist."
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return MatrixClientPeg.get().invite(roomId, addr);
|
return MatrixClientPeg.get().invite(roomId, addr);
|
||||||
|
@ -105,14 +122,109 @@ export default class MultiInviter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_doInvite(address, ignoreProfile) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
console.log(`Inviting ${address}`);
|
||||||
|
|
||||||
_inviteMore(nextIndex) {
|
let doInvite;
|
||||||
|
if (this.groupId !== null) {
|
||||||
|
doInvite = GroupStore.inviteUserToGroup(this.groupId, address);
|
||||||
|
} else {
|
||||||
|
doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
doInvite.then(() => {
|
||||||
|
if (this._canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completionStates[address] = 'invited';
|
||||||
|
delete this.errors[address];
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
}).catch((err) => {
|
||||||
|
if (this._canceled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorText;
|
||||||
|
let fatal = false;
|
||||||
|
if (err.errcode === 'M_FORBIDDEN') {
|
||||||
|
fatal = true;
|
||||||
|
errorText = _t('You do not have permission to invite people to this room.');
|
||||||
|
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
||||||
|
// we're being throttled so wait a bit & try again
|
||||||
|
setTimeout(() => {
|
||||||
|
this._doInvite(address, ignoreProfile).then(resolve, reject);
|
||||||
|
}, 5000);
|
||||||
|
return;
|
||||||
|
} else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'RIOT.USER_NOT_FOUND'].includes(err.errcode)) {
|
||||||
|
errorText = _t("User %(user_id)s does not exist", {user_id: address});
|
||||||
|
} else if (err.errcode === 'M_PROFILE_UNDISCLOSED') {
|
||||||
|
errorText = _t("User %(user_id)s may or may not exist", {user_id: address});
|
||||||
|
} else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) {
|
||||||
|
// Invite without the profile check
|
||||||
|
console.warn(`User ${address} does not have a profile - inviting anyways automatically`);
|
||||||
|
this._doInvite(address, true).then(resolve, reject);
|
||||||
|
} else {
|
||||||
|
errorText = _t('Unknown server error');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.completionStates[address] = 'error';
|
||||||
|
this.errors[address] = {errorText, errcode: err.errcode};
|
||||||
|
|
||||||
|
this.busy = !fatal;
|
||||||
|
this.fatal = fatal;
|
||||||
|
|
||||||
|
if (fatal) {
|
||||||
|
reject();
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_inviteMore(nextIndex, ignoreProfile) {
|
||||||
if (this._canceled) {
|
if (this._canceled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextIndex === this.addrs.length) {
|
if (nextIndex === this.addrs.length) {
|
||||||
this.busy = false;
|
this.busy = false;
|
||||||
|
if (Object.keys(this.errors).length > 0 && !this.groupId) {
|
||||||
|
// There were problems inviting some people - see if we can invite them
|
||||||
|
// without caring if they exist or not.
|
||||||
|
const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND', 'RIOT.USER_NOT_FOUND'];
|
||||||
|
const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode));
|
||||||
|
|
||||||
|
if (unknownProfileUsers.length > 0) {
|
||||||
|
const inviteUnknowns = () => {
|
||||||
|
const promises = unknownProfileUsers.map(u => this._doInvite(u, true));
|
||||||
|
Promise.all(promises).then(() => this.deferred.resolve(this.completionStates));
|
||||||
|
};
|
||||||
|
|
||||||
|
if (SettingsStore.getValue("alwaysInviteUnknownUsers", this.roomId)) {
|
||||||
|
inviteUnknowns();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog");
|
||||||
|
console.log("Showing failed to invite dialog...");
|
||||||
|
Modal.createTrackedDialog('Failed to invite the following users to the room', '', AskInviteAnywayDialog, {
|
||||||
|
unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}),
|
||||||
|
onInviteAnyways: () => inviteUnknowns(),
|
||||||
|
onGiveUp: () => {
|
||||||
|
// Fake all the completion states because we already warned the user
|
||||||
|
for (const addr of unknownProfileUsers) {
|
||||||
|
this.completionStates[addr] = 'invited';
|
||||||
|
}
|
||||||
|
this.deferred.resolve(this.completionStates);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
this.deferred.resolve(this.completionStates);
|
this.deferred.resolve(this.completionStates);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -134,48 +246,8 @@ export default class MultiInviter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let doInvite;
|
this._doInvite(addr, ignoreProfile).then(() => {
|
||||||
if (this.groupId !== null) {
|
this._inviteMore(nextIndex + 1, ignoreProfile);
|
||||||
doInvite = GroupStore.inviteUserToGroup(this.groupId, addr);
|
}).catch(() => this.deferred.resolve(this.completionStates));
|
||||||
} else {
|
|
||||||
doInvite = this._inviteToRoom(this.roomId, addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
doInvite.then(() => {
|
|
||||||
if (this._canceled) { return; }
|
|
||||||
|
|
||||||
this.completionStates[addr] = 'invited';
|
|
||||||
|
|
||||||
this._inviteMore(nextIndex + 1);
|
|
||||||
}).catch((err) => {
|
|
||||||
if (this._canceled) { return; }
|
|
||||||
|
|
||||||
let errorText;
|
|
||||||
let fatal = false;
|
|
||||||
if (err.errcode === 'M_FORBIDDEN') {
|
|
||||||
fatal = true;
|
|
||||||
errorText = _t('You do not have permission to invite people to this room.');
|
|
||||||
} else if (err.errcode === 'M_LIMIT_EXCEEDED') {
|
|
||||||
// we're being throttled so wait a bit & try again
|
|
||||||
setTimeout(() => {
|
|
||||||
this._inviteMore(nextIndex);
|
|
||||||
}, 5000);
|
|
||||||
return;
|
|
||||||
} else if(err.errcode === "M_NOT_FOUND") {
|
|
||||||
errorText = _t("User %(user_id)s does not exist", {user_id: addr});
|
|
||||||
} else {
|
|
||||||
errorText = _t('Unknown server error');
|
|
||||||
}
|
|
||||||
this.completionStates[addr] = 'error';
|
|
||||||
this.errorTexts[addr] = errorText;
|
|
||||||
this.busy = !fatal;
|
|
||||||
this.fatal = fatal;
|
|
||||||
|
|
||||||
if (!fatal) {
|
|
||||||
this._inviteMore(nextIndex + 1);
|
|
||||||
} else {
|
|
||||||
this.deferred.resolve(this.completionStates);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ Once a timer is finished or aborted, it can't be started again
|
||||||
a new one through `clone()` or `cloneIfRun()`.
|
a new one through `clone()` or `cloneIfRun()`.
|
||||||
*/
|
*/
|
||||||
export default class Timer {
|
export default class Timer {
|
||||||
|
|
||||||
constructor(timeout) {
|
constructor(timeout) {
|
||||||
this._timeout = timeout;
|
this._timeout = timeout;
|
||||||
this._onTimeout = this._onTimeout.bind(this);
|
this._onTimeout = this._onTimeout.bind(this);
|
||||||
|
@ -70,6 +69,7 @@ export default class Timer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* if not started before, starts the timer.
|
* if not started before, starts the timer.
|
||||||
|
* @returns {Timer} the same timer
|
||||||
*/
|
*/
|
||||||
start() {
|
start() {
|
||||||
if (!this.isRunning()) {
|
if (!this.isRunning()) {
|
||||||
|
@ -81,6 +81,7 @@ export default class Timer {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* (re)start the timer. If it's running, reset the timeout. If not, start it.
|
* (re)start the timer. If it's running, reset the timeout. If not, start it.
|
||||||
|
* @returns {Timer} the same timer
|
||||||
*/
|
*/
|
||||||
restart() {
|
restart() {
|
||||||
if (this.isRunning()) {
|
if (this.isRunning()) {
|
||||||
|
@ -98,6 +99,7 @@ export default class Timer {
|
||||||
/**
|
/**
|
||||||
* if the timer is running, abort it,
|
* if the timer is running, abort it,
|
||||||
* and reject the promise for this timer.
|
* and reject the promise for this timer.
|
||||||
|
* @returns {Timer} the same timer
|
||||||
*/
|
*/
|
||||||
abort() {
|
abort() {
|
||||||
if (this.isRunning()) {
|
if (this.isRunning()) {
|
||||||
|
|
Loading…
Reference in a new issue