Merge branch 'experimental' into bwindels/smarterresizer

This commit is contained in:
Bruno Windels 2019-01-15 10:23:50 +01:00
commit 1bbf1502ec
62 changed files with 1703 additions and 420 deletions

View file

@ -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

View file

@ -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

View file

@ -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",

View file

@ -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";

View 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;
}

View file

@ -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

View file

@ -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;
} }

View 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;
}

View file

@ -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;
} }

View 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

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -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

View 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

View file

@ -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

View 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
View 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

View file

@ -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);
} }
} }

View file

@ -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",

View file

@ -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,
}; };

View file

@ -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

View file

@ -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}

View file

@ -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,15 +24,21 @@ 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' });
} }
onSetupClick = async() => { onSetupClick = async () => {
// TODO: Should change to a restore key backup flow that checks the // TODO: Should change to a restore key backup flow that checks the
// recovery passphrase while at the same time also cross-signing the // recovery passphrase while at the same time also cross-signing the
// device as well in a single flow. Since we don't have that yet, we'll // device as well in a single flow. Since we don't have that yet, we'll
@ -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>
); );
} }

View file

@ -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>
);
}
}

View file

@ -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()) {

View file

@ -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);
}); });
} }

View file

@ -61,7 +61,7 @@ export default class CommunityProvider extends AutocompleteProvider {
if (command) { if (command) {
const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join'); const joinedGroups = cli.getGroups().filter(({myMembership}) => myMembership === 'join');
const groups = (await Promise.all(joinedGroups.map(async({groupId}) => { const groups = (await Promise.all(joinedGroups.map(async ({groupId}) => {
try { try {
return FlairStore.getGroupProfileCached(cli, groupId); return FlairStore.getGroupProfileCached(cli, groupId);
} catch (e) { // if FlairStore failed, fall back to just groupId } catch (e) { // if FlairStore failed, fall back to just groupId

View file

@ -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([], {

View 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>);
}
}

View file

@ -781,7 +781,7 @@ export default React.createClass({
), ),
button: _t("Leave"), button: _t("Leave"),
danger: this.state.isUserPrivileged, danger: this.state.isUserPrivileged,
onFinished: async(confirmed) => { onFinished: async (confirmed) => {
if (!confirmed) return; if (!confirmed) return;
this.setState({membershipBusy: true}); this.setState({membershipBusy: true});

View file

@ -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}

View file

@ -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;
} }

View file

@ -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

View file

@ -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}

View file

@ -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}

View file

@ -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
@ -835,7 +836,7 @@ module.exports = React.createClass({
SettingsStore.getLabsFeatures().forEach((featureId) => { SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need // TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render // to rebind the onChange each time we render
const onChange = async(e) => { const onChange = async (e) => {
const checked = e.target.checked; const checked = e.target.checked;
if (featureId === "feature_lazyloading") { if (featureId === "feature_lazyloading") {
const confirmed = await this._onLazyLoadChanging(checked); const confirmed = await this._onLazyLoadChanging(checked);

View file

@ -48,7 +48,7 @@ export default class GroupInviteTileContextMenu extends React.Component {
Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, { Modal.createTrackedDialog('Reject community invite', '', QuestionDialog, {
title: _t('Reject invitation'), title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'), description: _t('Are you sure you want to reject the invitation?'),
onFinished: async(shouldLeave) => { onFinished: async (shouldLeave) => {
if (!shouldLeave) return; if (!shouldLeave) return;
// FIXME: controller shouldn't be loading a view :( // FIXME: controller shouldn't be loading a view :(

View file

@ -35,7 +35,7 @@ export default class StatusMessageContextMenu extends React.Component {
}; };
} }
_onClearClick = async(e) => { _onClearClick = async (e) => {
await MatrixClientPeg.get()._unstable_setStatusMessage(""); await MatrixClientPeg.get()._unstable_setStatusMessage("");
this.setState({message: ""}); this.setState({message: ""});
}; };

View file

@ -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" />

View file

@ -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,

View 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>
);
},
});

View file

@ -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,

View file

@ -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 }

View file

@ -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) {

View file

@ -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,8 +713,8 @@ 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
// we're only inviting one user. // we're only inviting one user.

View file

@ -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,
}; };

View file

@ -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({
@ -1260,7 +1269,7 @@ export default class MessageComposerInput extends React.Component {
} }
}; };
selectHistory = async(up) => { selectHistory = async (up) => {
const delta = up ? -1 : 1; const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message // True if we are not currently selecting history, but composing a message
@ -1308,7 +1317,7 @@ export default class MessageComposerInput extends React.Component {
return true; return true;
}; };
onTab = async(e) => { onTab = async (e) => {
this.setState({ this.setState({
someCompletions: null, someCompletions: null,
}); });
@ -1330,7 +1339,7 @@ export default class MessageComposerInput extends React.Component {
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow(); up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
}; };
onEscape = async(e) => { onEscape = async (e) => {
e.preventDefault(); e.preventDefault();
if (this.autocomplete) { if (this.autocomplete) {
this.autocomplete.onEscape(e); this.autocomplete.onEscape(e);
@ -1349,7 +1358,7 @@ export default class MessageComposerInput extends React.Component {
/* If passed null, restores the original editor content from state.originalEditorState. /* If passed null, restores the original editor content from state.originalEditorState.
* If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState. * If passed a non-null displayedCompletion, modifies state.originalEditorState to compute new state.editorState.
*/ */
setDisplayedCompletion = async(displayedCompletion: ?Completion): boolean => { setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => {
const activeEditorState = this.state.originalEditorState || this.state.editorState; const activeEditorState = this.state.originalEditorState || this.state.editorState;
if (displayedCompletion == null) { if (displayedCompletion == null) {
@ -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}

View file

@ -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 });
} }

View file

@ -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>
); );

View file

@ -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();
}, },

View file

@ -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();

View file

@ -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,
}); });
}, },

View file

@ -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"

View file

@ -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;

View file

@ -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
View 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);
}
}
}

View file

@ -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'),

View 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;

View file

@ -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;

View file

@ -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);
}
});
} }
} }

View file

@ -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()) {