Merge branch 'experimental' of github.com:matrix-org/matrix-react-sdk into erikj/state_counters

This commit is contained in:
Erik Johnston 2019-01-10 16:56:47 +00:00
commit cbf9ff6aee
58 changed files with 1168 additions and 320 deletions

View file

@ -18,7 +18,6 @@ src/components/structures/ScrollPanel.js
src/components/structures/SearchBox.js
src/components/structures/TimelinePanel.js
src/components/structures/UploadBar.js
src/components/structures/UserSettings.js
src/components/views/avatars/BaseAvatar.js
src/components/views/avatars/MemberAvatar.js
src/components/views/create_room/RoomAlias.js

View file

@ -14,8 +14,22 @@ node_js:
addons:
chrome: stable
install:
- npm install
# install synapse prerequisites for end to end tests
- 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:
./scripts/travis.sh
- ./scripts/travis/install-deps.sh
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
script:
- ./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",
"lint": "eslint src/",
"lintall": "eslint src/ test/",
"lintwithexclusions": "eslint --max-warnings 20 --ignore-path .eslintignore.errorfiles src test",
"lintwithexclusions": "eslint --max-warnings 19 --ignore-path .eslintignore.errorfiles src test",
"clean": "rimraf lib",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt",
"test": "karma start --single-run=true --browsers ChromeHeadless",
@ -118,7 +118,7 @@
"babel-preset-react": "^6.24.1",
"chokidar": "^1.6.1",
"concurrently": "^4.0.1",
"eslint": "^5.8.0",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.2.1",
"eslint-plugin-flowtype": "^2.30.0",

View file

@ -5,6 +5,7 @@
@import "./structures/_ContextualMenu.scss";
@import "./structures/_CreateRoom.scss";
@import "./structures/_FilePanel.scss";
@import "./structures/_GroupGridView.scss";
@import "./structures/_GroupView.scss";
@import "./structures/_HomePage.scss";
@import "./structures/_LeftPanel.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

@ -41,10 +41,6 @@ limitations under the License.
cursor: pointer;
}
.mx_LeftPanel_callView {
}
.mx_LeftPanel {
flex: 1;
overflow-x: hidden;

View file

@ -73,14 +73,16 @@ limitations under the License.
.mx_MatrixChat > :not(.mx_LeftPanel_container):not(.mx_ResizeHandle) {
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
and https://github.com/vector-im/vector-web/issues/946.
Empirically this stops the MessagePanel's width exploding outwards when
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
needed height 100% all the way down to the HomePage. Height does not

View file

@ -14,10 +14,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
.mx_CreateKeyBackupDialog {
padding-right: 40px;
}
.mx_CreateKeyBackupDialog .mx_Dialog_title {
/* TODO: Consider setting this for all dialog titles. */
margin-bottom: 1em;

View file

@ -24,7 +24,7 @@ limitations under the License.
padding-bottom: 10px;
&:before {
mask: url("../../../img/e2e/lock-warning.svg");
mask: url("../../img/e2e/lock-warning.svg");
mask-repeat: no-repeat;
background-color: $primary-fg-color;
content: "";

View file

@ -19,6 +19,11 @@
cursor: pointer;
}
/* More specific to override `.markdown-body a` text-decoration */
.mx_EventTile_content .markdown-body a.mx_Pill {
text-decoration: none;
}
/* More specific to override `.markdown-body a` color */
.mx_EventTile_content .markdown-body a.mx_UserPill,
.mx_UserPill {
@ -31,7 +36,9 @@
background-color: $accent-color ! important;
}
/* More specific to override `.markdown-body a` color */
.mx_EventTile_highlight .mx_EventTile_content .markdown-body a.mx_UserPill_me,
.mx_EventTile_content .markdown-body a.mx_AtRoomPill,
.mx_EventTile_content .mx_AtRoomPill,
.mx_MessageComposer_input .mx_AtRoomPill {
color: $accent-fg-color;

View file

@ -56,4 +56,6 @@ limitations under the License.
border-radius: 5px;
background: $imagebody-giflabel;
border: 2px solid $imagebody-giflabel-border;
color: $imagebody-giflabel-color;
pointer-events: none;
}

View file

@ -53,6 +53,10 @@ limitations under the License.
.mx_MemberList_query,
.mx_GroupMemberList_query,
.mx_GroupRoomList_query {
flex: 0 0 auto;
}
.mx_MemberList .gm-scrollbar-container {
flex: 1 1 0;
}

View file

@ -54,12 +54,14 @@ limitations under the License.
align-items: center;
flex: 1;
vertical-align: middle;
min-width: 0;
}
.mx_RoomTile_labelContainer {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
.mx_RoomTile_subtext {
@ -99,7 +101,6 @@ limitations under the License.
}
.mx_RoomTile_name {
flex: 1 5 auto;
font-size: 14px;
font-weight: 600;
padding: 0 6px;

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,8 +162,13 @@ $lightbox-bg-color: #454545;
$lightbox-fg-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-border: rgba(1, 1, 1, 0.2);
$imagebody-giflabel-color: rgba(0, 0, 0, 1);
// unused?
$progressbar-color: #000;

View file

@ -99,6 +99,7 @@ $lightbox-background-bg-color: #000;
$imagebody-giflabel: rgba(0, 0, 0, 0.7);
$imagebody-giflabel-border: rgba(0, 0, 0, 0.2);
$imagebody-giflabel-color: rgba(255, 255, 255, 1);
$greyed-fg-color: #888;
@ -183,6 +184,9 @@ $lightbox-bg-color: #454545;
$lightbox-fg-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?
$progressbar-color: #000;

View file

@ -175,8 +175,13 @@ $lightbox-bg-color: #454545;
$lightbox-fg-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-border: rgba(0, 0, 0, 0.2);
$imagebody-giflabel-color: rgba(255, 255, 255, 1);
// unused?
$progressbar-color: #000;

View file

@ -24,18 +24,4 @@ rm -r node_modules/matrix-react-sdk
ln -s "$REACT_SDK_DIR" node_modules/matrix-react-sdk
npm run build
npm run test
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
set -ex
npm install
scripts/fetchdep.sh matrix-org matrix-js-sdk
rm -r node_modules/matrix-js-sdk || true
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
npm install
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.
*/
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
* changes, you can subscribe to only changes relevant to a particular
* room.
@ -28,11 +28,15 @@ import RoomViewStore from './stores/RoomViewStore';
class ActiveRoomObserver {
constructor() {
this._listeners = {};
this._activeRoomId = RoomViewStore.getRoomId();
const roomStore = OpenRoomsStore.getActiveRoomStore();
this._activeRoomId = roomStore && roomStore.getRoomId();
// TODO: We could self-destruct when the last listener goes away, or at least
// 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) {
@ -51,23 +55,23 @@ class ActiveRoomObserver {
}
}
_emit(roomId) {
_emit(roomId, newActiveRoomId) {
if (!this._listeners[roomId]) return;
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
if (this._activeRoomId) this._emit(this._activeRoomId);
if (this._activeRoomId) this._emit(this._activeRoomId, newActiveRoomId);
// update our cache
this._activeRoomId = RoomViewStore.getRoomId();
this._activeRoomId = newActiveRoomId;
// 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 {
HomePage: "home_page",
RoomView: "room_view",
GroupGridView: "group_grid_view",
UserSettings: "user_settings",
RoomDirectory: "room_directory",
UserView: "user_view",

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 before the user becomes active, in which case it is only started
* later on when the user does become active.
* @param {Timer} timer the timer to use
*/
timeWhileActive(timer) {
// 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");
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 { _t, _td } from '../../../../languageHandler';
import { _t } from '../../../../languageHandler';
const PHASE_PASSPHRASE = 0;
const PHASE_PASSPHRASE_CONFIRM = 1;
@ -102,7 +102,7 @@ export default React.createClass({
info = await MatrixClientPeg.get().createKeyBackupVersion(
this._keyBackupInfo,
);
await MatrixClientPeg.get().backupAllGroupSessions(info.version);
await MatrixClientPeg.get().scheduleAllGroupSessionsForBackup();
this.setState({
phase: PHASE_DONE,
});
@ -344,7 +344,10 @@ export default React.createClass({
_renderPhaseShowKey: function() {
let bodyText;
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 {
bodyText = _t("As a safety net, you can use it to restore your encrypted message history.");
}
@ -352,7 +355,7 @@ export default React.createClass({
return <div>
<p>{_t("Make a copy of this Recovery Key and keep it safe.")}</p>
<p>{bodyText}</p>
<p className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_primaryContainer">
<div className="mx_CreateKeyBackupDialog_recoveryKeyHeader">
{_t("Your Recovery Key")}
</div>
@ -369,7 +372,7 @@ export default React.createClass({
</button>
</div>
</div>
</p>
</div>
</div>;
},
@ -405,7 +408,6 @@ export default React.createClass({
_renderBusyPhase: function(text) {
const Spinner = sdk.getComponent('views.elements.Spinner');
return <div>
<p>{_t(text)}</p>
<Spinner />
</div>;
},
@ -413,8 +415,10 @@ export default React.createClass({
_renderPhaseDone: function() {
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
return <div>
<p>{_t("Backup created")}</p>
<p>{_t("Your encryption keys are now being backed up to your Homeserver.")}</p>
<p>{_t(
"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')}
onPrimaryButtonClick={this._onDone}
hasCancel={false}
@ -451,7 +455,9 @@ export default React.createClass({
case PHASE_KEEPITSAFE:
return _t('Keep it safe');
case PHASE_BACKINGUP:
return _t('Backing up...');
return _t('Starting backup...');
case PHASE_DONE:
return _t('Backup Started');
default:
return _t("Create Key Backup");
}
@ -488,7 +494,7 @@ export default React.createClass({
content = this._renderPhaseKeepItSafe();
break;
case PHASE_BACKINGUP:
content = this._renderBusyPhase(_td("Backing up..."));
content = this._renderBusyPhase();
break;
case PHASE_DONE:
content = this._renderPhaseDone();
@ -503,7 +509,7 @@ export default React.createClass({
<BaseDialog className='mx_CreateKeyBackupDialog'
onFinished={this.props.onFinished}
title={this._titleForPhase(this.state.phase)}
hasCancel={[PHASE_DONE].includes(this.state.phase)}
hasCancel={[PHASE_PASSPHRASE, PHASE_DONE].includes(this.state.phase)}
>
<div>
{content}

View file

@ -32,7 +32,7 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
dis.dispatch({ action: 'view_user_settings' });
}
onSetupClick = async() => {
onSetupClick = async () => {
// TODO: Should change to a restore key backup flow that checks 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
@ -79,7 +79,6 @@ export default class NewRecoveryMethodDialog extends React.PureComponent {
<BaseDialog className="mx_NewRecoveryMethodDialog"
onFinished={this.props.onFinished}
title={title}
hasCancel={false}
>
<div>
<p>{_t(

View file

@ -61,7 +61,7 @@ export default class CommunityProvider extends AutocompleteProvider {
if (command) {
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 {
return FlairStore.getGroupProfileCached(cli, groupId);
} catch (e) { // if FlairStore failed, fall back to just groupId

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"),
danger: this.state.isUserPrivileged,
onFinished: async(confirmed) => {
onFinished: async (confirmed) => {
if (!confirmed) return;
this.setState({membershipBusy: true});

View file

@ -31,6 +31,7 @@ import sessionStore from '../../stores/SessionStore';
import MatrixClientPeg from '../../MatrixClientPeg';
import SettingsStore from "../../settings/SettingsStore";
import RoomListStore from "../../stores/RoomListStore";
import OpenRoomsStore from "../../stores/OpenRoomsStore";
import TagOrderActions from '../../actions/TagOrderActions';
import RoomListActions from '../../actions/RoomListActions';
@ -416,6 +417,7 @@ const LoggedInView = React.createClass({
const RoomDirectory = sdk.getComponent('structures.RoomDirectory');
const HomePage = sdk.getComponent('structures.HomePage');
const GroupView = sdk.getComponent('structures.GroupView');
const GroupGridView = sdk.getComponent('structures.GroupGridView');
const MyGroups = sdk.getComponent('structures.MyGroups');
const MatrixToolbar = sdk.getComponent('globals.MatrixToolbar');
const CookieBar = sdk.getComponent('globals.CookieBar');
@ -428,7 +430,14 @@ const LoggedInView = React.createClass({
switch (this.props.page_type) {
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
roomViewStore={OpenRoomsStore.getActiveRoomStore()}
ref='roomView'
autoJoin={this.props.autoJoin}
onRegistered={this.props.onRegistered}
@ -442,7 +451,9 @@ const LoggedInView = React.createClass({
ConferenceHandler={this.props.ConferenceHandler}
/>;
break;
case PageTypes.GroupGridView:
page_element = <GroupGridView collapsedRhs={this.props.collapsedRhs} />;
break;
case PageTypes.UserSettings:
page_element = <UserSettings
onClose={this.props.onCloseAllSettings}

View file

@ -71,14 +71,13 @@ export default class MainSplit extends React.Component {
}
componentDidUpdate(prevProps) {
const wasExpanded = !this.props.collapsedRhs && prevProps.collapsedRhs;
const wasCollapsed = this.props.collapsedRhs && !prevProps.collapsedRhs;
const wasPanelSet = this.props.panel && !prevProps.panel;
const wasPanelCleared = !this.props.panel && prevProps.panel;
const shouldAllowResizing =
!this.props.collapsedRhs &&
this.props.panel;
if (wasExpanded || wasPanelSet) {
if (shouldAllowResizing && !this.resizer) {
this._createResizer();
} else if (wasCollapsed || wasPanelCleared) {
} else if (!shouldAllowResizing && this.resizer) {
this.resizer.detach();
this.resizer = null;
}

View file

@ -651,6 +651,9 @@ export default React.createClass({
case 'view_group':
this._viewGroup(payload);
break;
case 'group_grid_view':
this._viewGroupGrid(payload);
break;
case 'view_home_page':
this._viewHome();
break;
@ -862,6 +865,7 @@ export default React.createClass({
// room name and avatar from an invite email)
_viewRoom: function(roomInfo) {
this.focusComposer = true;
console.log("!!! MatrixChat._viewRoom", roomInfo);
const newState = {
currentRoomId: roomInfo.room_id || null,
@ -910,6 +914,9 @@ export default React.createClass({
if (roomInfo.event_id && roomInfo.highlighted) {
presentedId += "/" + roomInfo.event_id;
}
// TODO: only emit this when we're not in grid mode?
this.notifyNewScreen('room/' + presentedId);
newState.ready = true;
this.setState(newState);
@ -926,6 +933,11 @@ export default React.createClass({
this.notifyNewScreen('group/' + groupId);
},
_viewGroupGrid: function(payload) {
this._setPage(PageTypes.GroupGridView);
// this.notifyNewScreen('grid/' + payload.group_id);
},
_viewHome: function() {
// The home page requires the "logged in" view, so we'll set that.
this.setStateForNewView({

View file

@ -165,7 +165,7 @@ export default class RightPanel extends React.Component {
} else if (this.state.phase === RightPanel.Phase.GroupRoomList) {
panel = <GroupRoomList groupId={this.props.groupId} key={this.props.groupId} />;
} 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) {
panel = <GroupMemberInfo
groupMember={this.state.member}

View file

@ -1,7 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket 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");
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 sdk = require('../../index');
const CallHandler = require('../../CallHandler');
const dis = require("../../dispatcher");
const Tinter = require("../../Tinter");
const rate_limited_func = require('../../ratelimitedfunc');
const ObjectUtils = require('../../ObjectUtils');
@ -46,7 +45,6 @@ import { KeyCode, isOnlyCtrlOrCmdKeyEvent } from '../../Keyboard';
import MainSplit from './MainSplit';
import RightPanel from './RightPanel';
import RoomViewStore from '../../stores/RoomViewStore';
import RoomScrollStateStore from '../../stores/RoomScrollStateStore';
import WidgetEchoStore from '../../stores/WidgetEchoStore';
import SettingsStore, {SettingLevel} from "../../settings/SettingsStore";
@ -94,6 +92,8 @@ module.exports = React.createClass({
// Servers the RoomView can use to try and assist joins
viaServers: PropTypes.arrayOf(PropTypes.string),
// the store for this room view
roomViewStore: PropTypes.object.isRequired,
},
getInitialState: function() {
@ -155,7 +155,7 @@ module.exports = React.createClass({
},
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.timeline", this.onRoomTimeline);
MatrixClientPeg.get().on("Room.name", this.onRoomName);
@ -166,7 +166,7 @@ module.exports = React.createClass({
MatrixClientPeg.get().on("crypto.keyBackupStatus", this.onKeyBackupStatus);
this._fetchMediaConfig();
// Start listening for RoomViewStore updates
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate(true);
WidgetEchoStore.on('update', this._onWidgetEchoStoreUpdate);
@ -197,8 +197,8 @@ module.exports = React.createClass({
if (this.unmounted) {
return;
}
if (!initial && this.state.roomId !== RoomViewStore.getRoomId()) {
const store = this.props.roomViewStore;
if (!initial && this.state.roomId !== store.getRoomId()) {
// RoomView explicitly does not support changing what room
// is being viewed: instead it should just be re-mounted when
// 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.
return;
}
const newState = {
roomId: RoomViewStore.getRoomId(),
roomAlias: RoomViewStore.getRoomAlias(),
roomLoading: RoomViewStore.isRoomLoading(),
roomLoadError: RoomViewStore.getRoomLoadError(),
joining: RoomViewStore.isJoining(),
initialEventId: RoomViewStore.getInitialEventId(),
isInitialEventHighlighted: RoomViewStore.isInitialEventHighlighted(),
forwardingEvent: RoomViewStore.getForwardingEvent(),
shouldPeek: RoomViewStore.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", RoomViewStore.getRoomId()),
editingRoomSettings: RoomViewStore.isEditingSettings(),
roomId: store.getRoomId(),
roomAlias: store.getRoomAlias(),
roomLoading: store.isRoomLoading(),
roomLoadError: store.getRoomLoadError(),
joining: store.isJoining(),
initialEventId: store.getInitialEventId(),
isInitialEventHighlighted: store.isInitialEventHighlighted(),
forwardingEvent: store.getForwardingEvent(),
shouldPeek: store.shouldPeek(),
showingPinned: SettingsStore.getValue("PinnedEvents.isOpen", store.getRoomId()),
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
console.log(
@ -389,7 +388,7 @@ module.exports = React.createClass({
// XXX: EVIL HACK to autofocus inviting on empty rooms.
// 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.getLiveTimeline() &&
this.state.room.getLiveTimeline().getEvents() &&
@ -443,7 +442,7 @@ module.exports = React.createClass({
roomView.removeEventListener('dragleave', this.onDragLeaveOrEnd);
roomView.removeEventListener('dragend', this.onDragLeaveOrEnd);
}
dis.unregister(this.dispatcherRef);
this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef);
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room", this.onRoom);
MatrixClientPeg.get().removeListener("Room.timeline", this.onRoomTimeline);
@ -611,17 +610,10 @@ module.exports = React.createClass({
}
},
async onRoomRecoveryReminderFinished(backupCreated) {
// If the user cancelled the key backup dialog, it suggests they don't
// want to be reminded anymore.
if (!backupCreated) {
await SettingsStore.setValue(
"showRoomRecoveryReminder",
null,
SettingLevel.ACCOUNT,
false,
);
}
onRoomRecoveryReminderDontAskAgain: function() {
// Called when the option to not ask again is set:
// force an update to hide the recovery reminder
this.forceUpdate();
},
onKeyBackupStatus() {
@ -842,7 +834,7 @@ module.exports = React.createClass({
},
onSearchResultsResize: function() {
dis.dispatch({ action: 'timeline_resize' }, true);
this.props.roomViewStore.getDispatcher().dispatch({ action: 'timeline_resize' }, true);
},
onSearchResultsFillRequest: function(backwards) {
@ -863,7 +855,7 @@ module.exports = React.createClass({
onInviteButtonClick: function() {
// call AddressPickerDialog
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'view_invite',
roomId: this.state.room.roomId,
});
@ -885,7 +877,7 @@ module.exports = React.createClass({
// Join this room once the user has registered and logged in
const signUrl = this.props.thirdPartyInvite ?
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'do_after_sync_prepared',
deferred_action: {
action: 'join_room',
@ -895,7 +887,7 @@ module.exports = React.createClass({
// Don't peek whilst registering otherwise getPendingEventList complains
// Do this by indicating our intention to join
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'will_join',
});
@ -906,20 +898,20 @@ module.exports = React.createClass({
if (submitted) {
this.props.onRegistered(credentials);
} else {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'cancel_after_sync_prepared',
});
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'cancel_join',
});
}
},
onDifferentServerClicked: (ev) => {
dis.dispatch({action: 'start_registration'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'start_registration'});
close();
},
onLoginClick: (ev) => {
dis.dispatch({action: 'start_login'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'start_login'});
close();
},
}).close;
@ -929,7 +921,7 @@ module.exports = React.createClass({
Promise.resolve().then(() => {
const signUrl = this.props.thirdPartyInvite ?
this.props.thirdPartyInvite.inviteSignUrl : undefined;
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'join_room',
opts: { inviteSignUrl: signUrl, viaServers: this.props.viaServers },
});
@ -994,10 +986,10 @@ module.exports = React.createClass({
},
uploadFile: async function(file) {
dis.dispatch({action: 'focus_composer'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
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.
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'message_sent',
});
},
injectSticker: function(url, info, text) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
return;
}
@ -1229,7 +1221,7 @@ module.exports = React.createClass({
},
onSettingsClick: function() {
dis.dispatch({ action: 'open_room_settings' });
this.props.roomViewStore.getDispatcher().dispatch({ action: 'open_room_settings' });
},
onSettingsSaveClick: function() {
@ -1262,31 +1254,31 @@ module.exports = React.createClass({
});
// still editing room settings
} else {
dis.dispatch({ action: 'close_settings' });
this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' });
}
}).finally(() => {
this.setState({
uploadingRoomSettings: false,
});
dis.dispatch({ action: 'close_settings' });
this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' });
}).done();
},
onCancelClick: function() {
console.log("updateTint from onCancelClick");
this.updateTint();
dis.dispatch({ action: 'close_settings' });
this.props.roomViewStore.getDispatcher().dispatch({ action: 'close_settings' });
if (this.state.forwardingEvent) {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'forward_event',
event: null,
});
}
dis.dispatch({action: 'focus_composer'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'focus_composer'});
},
onLeaveClick: function() {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'leave_room',
room_id: this.state.room.roomId,
});
@ -1294,7 +1286,7 @@ module.exports = React.createClass({
onForgetClick: 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) {
const errCode = err.errcode || _t("unknown error code");
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
@ -1311,7 +1303,7 @@ module.exports = React.createClass({
rejecting: true,
});
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({
rejecting: false,
});
@ -1337,7 +1329,7 @@ module.exports = React.createClass({
// using /leave rather than /join. In the short term though, we
// just ignore them.
// https://github.com/vector-im/vector-web/issues/1134
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
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
jumpToLiveTimeline: function() {
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
@ -1446,7 +1438,7 @@ module.exports = React.createClass({
},
onFullscreenClick: function() {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'video_fullscreen',
fullscreen: true,
}, true);
@ -1571,6 +1563,7 @@ module.exports = React.createClass({
<RoomHeader ref="header"
room={this.state.room}
oobData={this.props.oobData}
isGrid={this.props.isGrid}
collapsedRhs={this.props.collapsedRhs}
/>
<div className="mx_RoomView_body">
@ -1617,6 +1610,7 @@ module.exports = React.createClass({
<div className="mx_RoomView">
<RoomHeader
ref="header"
isGrid={this.props.isGrid}
room={this.state.room}
collapsedRhs={this.props.collapsedRhs}
/>
@ -1704,7 +1698,7 @@ module.exports = React.createClass({
aux = <RoomUpgradeWarningBar room={this.state.room} />;
hideCancel = true;
} else if (showRoomRecoveryReminder) {
aux = <RoomRecoveryReminder onFinished={this.onRoomRecoveryReminderFinished} />;
aux = <RoomRecoveryReminder onDontAskAgainSet={this.onRoomRecoveryReminderDontAskAgain} />;
hideCancel = true;
} else if (this.state.showingPinned) {
hideCancel = true; // has own cancel
@ -1758,7 +1752,9 @@ module.exports = React.createClass({
if (canSpeak) {
messageComposer =
<MessageComposer
roomViewStore={this.props.roomViewStore}
room={this.state.room}
isGrid={this.props.isGrid}
onResize={this.onChildResize}
uploadFile={this.uploadFile}
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 (
<main className={"mx_RoomView" + (inCall ? " mx_RoomView_inCall" : "")} ref="roomView">
<RoomHeader ref="header" room={this.state.room} searchInfo={searchInfo}
isGrid={this.props.isGrid}
oobData={this.props.oobData}
editing={this.state.editingRoomSettings}
saving={this.state.uploadingRoomSettings}

View file

@ -835,7 +835,7 @@ module.exports = React.createClass({
SettingsStore.getLabsFeatures().forEach((featureId) => {
// TODO: this ought to be a separate component so that we don't need
// to rebind the onChange each time we render
const onChange = async(e) => {
const onChange = async (e) => {
const checked = e.target.checked;
if (featureId === "feature_lazyloading") {
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, {
title: _t('Reject invitation'),
description: _t('Are you sure you want to reject the invitation?'),
onFinished: async(shouldLeave) => {
onFinished: async (shouldLeave) => {
if (!shouldLeave) return;
// 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("");
this.setState({message: ""});
};

View file

@ -21,6 +21,7 @@ import dis from '../../../dispatcher';
import TagOrderActions from '../../../actions/TagOrderActions';
import MatrixClientPeg from '../../../MatrixClientPeg';
import sdk from '../../../index';
import SettingsStore from "../../../settings/SettingsStore";
export default class TagTileContextMenu extends React.Component {
static propTypes = {
@ -34,6 +35,7 @@ export default class TagTileContextMenu extends React.Component {
this._onViewCommunityClick = this._onViewCommunityClick.bind(this);
this._onRemoveClick = this._onRemoveClick.bind(this);
this._onViewAsGridClick = this._onViewAsGridClick.bind(this);
}
_onViewCommunityClick() {
@ -53,8 +55,28 @@ export default class TagTileContextMenu extends React.Component {
this.props.onFinished();
}
_onViewAsGridClick() {
dis.dispatch({
action: 'group_grid_view',
group_id: this.props.tag,
});
this.props.onFinished();
}
render() {
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>
<div className="mx_TagTileContextMenu_item" onClick={this._onViewCommunityClick} >
<TintableSvg
@ -65,6 +87,7 @@ export default class TagTileContextMenu extends React.Component {
/>
{ _t('View Community') }
</div>
{ gridViewOption }
<hr className="mx_TagTileContextMenu_separator" />
<div className="mx_TagTileContextMenu_item" onClick={this._onRemoveClick} >
<img className="mx_TagTileContextMenu_item_icon" src="img/icon_context_delete.svg" width="15" height="15" />

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");
you may not use this file except in compliance with the License.
@ -15,48 +15,150 @@ limitations under the License.
*/
import React from 'react';
import QuestionDialog from './QuestionDialog';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import SettingsStore from "../../../settings/SettingsStore";
export default (props) => {
const description = _t("For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.");
export default class LogoutDialog extends React.Component {
constructor() {
super();
this._onSettingsLinkClick = this._onSettingsLinkClick.bind(this);
this._onExportE2eKeysClicked = this._onExportE2eKeysClicked.bind(this);
this._onFinished = this._onFinished.bind(this);
this._onSetRecoveryMethodClick = this._onSetRecoveryMethodClick.bind(this);
this._onLogoutConfirm = this._onLogoutConfirm.bind(this);
}
const onExportE2eKeysClicked = () => {
_onSettingsLinkClick() {
// close dialog
if (this.props.onFinished) {
this.props.onFinished();
}
}
_onExportE2eKeysClicked() {
Modal.createTrackedDialogAsync('Export E2E Keys', '',
import('../../../async-components/views/dialogs/ExportE2eKeysDialog'),
{
matrixClient: MatrixClientPeg.get(),
},
);
};
}
const onFinished = (confirmed) => {
_onFinished(confirmed) {
if (confirmed) {
dis.dispatch({action: 'logout'});
}
// close dialog
if (props.onFinished) {
props.onFinished();
if (this.props.onFinished) {
this.props.onFinished();
}
};
}
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
description={<div>{description}</div>}
button={_t("Sign out")}
extraButtons={[
(<button key="export" className="mx_Dialog_primary"
onClick={onExportE2eKeysClicked}>
{ _t("Export E2E room keys") }
</button>),
]}
onFinished={onFinished}
/>);
};
_onSetRecoveryMethodClick() {
Modal.createTrackedDialogAsync('Key Backup', 'Key Backup',
import('../../../async-components/views/dialogs/keybackup/CreateKeyBackupDialog'),
);
// close dialog
if (this.props.onFinished) {
this.props.onFinished();
}
}
_onLogoutConfirm() {
dis.dispatch({action: 'logout'});
// close dialog
if (this.props.onFinished) {
this.props.onFinished();
}
}
render() {
let description;
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
description = <div>
<p>{_t(
"When you log out, you'll lose your secure message history. To prevent " +
"this, set up a recovery method.",
)}</p>
<p>{_t(
"Alternatively, advanced users can also manually export encryption keys in " +
"<a>Settings</a> before logging out.", {},
{
a: sub => <a href='#/settings' onClick={this._onSettingsLinkClick}>{sub}</a>,
},
)}</p>
</div>;
} else {
description = <div>{_t(
"For security, logging out will delete any end-to-end " +
"encryption keys from this browser. If you want to be able " +
"to decrypt your conversation history from future Riot sessions, " +
"please export your room keys for safe-keeping.",
)}</div>;
}
if (SettingsStore.isFeatureEnabled("feature_keybackup")) {
if (!MatrixClientPeg.get().getKeyBackupEnabled()) {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');
const DialogButtons = sdk.getComponent('views.elements.DialogButtons');
// Not quite a standard question dialog as the primary button cancels
// the action and does something else instead, whilst non-default button
// confirms the action.
return (<BaseDialog
title={_t("Warning!")}
contentId='mx_Dialog_content'
hasCancel={false}
onFinsihed={this._onFinished}
>
<div className="mx_Dialog_content" id='mx_Dialog_content'>
{ description }
</div>
<DialogButtons primaryButton={_t('Set a Recovery Method')}
hasCancel={false}
onPrimaryButtonClick={this._onSetRecoveryMethodClick}
focus={true}
>
<button onClick={this._onLogoutConfirm}>
{_t("I understand, log out without")}
</button>
</DialogButtons>
</BaseDialog>);
} else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
// TODO: This is made up by me and would need to also mention verifying
// once you can restorew a backup by verifying a device
description={_t(
"When signing in again, you can access encrypted chat history by " +
"restoring your key backup. You'll need your recovery key.",
)}
button={_t("Sign out")}
onFinished={this._onFinished}
/>);
}
} else {
const QuestionDialog = sdk.getComponent('views.dialogs.QuestionDialog');
return (<QuestionDialog
hasCancelButton={true}
title={_t("Sign out")}
description={description}
button={_t("Sign out")}
extraButtons={[
(<button key="export" className="mx_Dialog_primary"
onClick={this._onExportE2eKeysClicked}>
{ _t("Export E2E room keys") }
</button>),
]}
onFinished={this._onFinished}
/>);
}
}
}

View file

@ -271,7 +271,7 @@ const Pill = React.createClass({
break;
}
const classes = classNames(pillClass, {
const classes = classNames("mx_Pill", pillClass, {
"mx_UserPill_me": userId === MatrixClientPeg.get().credentials.userId,
"mx_UserPill_selected": this.props.isSelected,
});

View file

@ -78,7 +78,6 @@ export default class HeaderButtons extends React.Component {
// till show_right_panel, just without the fromHeader flag
// as that would hide the right panel again
dis.dispatch(Object.assign({}, payload, {fromHeader: false}));
}
this.setState({
phase: payload.phase,

View file

@ -39,7 +39,6 @@ import Unread from '../../../Unread';
import { findReadReceiptFromUserId } from '../../../utils/Receipt';
import withMatrixClient from '../../../wrappers/withMatrixClient';
import AccessibleButton from '../elements/AccessibleButton';
import RoomViewStore from '../../../stores/RoomViewStore';
import SdkConfig from '../../../SdkConfig';
import MultiInviter from "../../../utils/MultiInviter";
import SettingsStore from "../../../settings/SettingsStore";
@ -50,6 +49,7 @@ module.exports = withMatrixClient(React.createClass({
propTypes: {
matrixClient: PropTypes.object.isRequired,
member: PropTypes.object.isRequired,
roomId: PropTypes.string,
},
getInitialState: function() {
@ -713,8 +713,8 @@ module.exports = withMatrixClient(React.createClass({
}
if (!member || !member.membership || member.membership === 'leave') {
const roomId = member && member.roomId ? member.roomId : RoomViewStore.getRoomId();
const onInviteUserButton = async() => {
const roomId = member && member.roomId ? member.roomId : this.props.roomId;
const onInviteUserButton = async () => {
try {
// We use a MultiInviter to re-use the invite logic, even though
// we're only inviting one user.

View file

@ -22,7 +22,6 @@ import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from '../../../Modal';
import sdk from '../../../index';
import dis from '../../../dispatcher';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import Stickerpicker from './Stickerpicker';
import { makeRoomPermalink } from '../../../matrix-to';
@ -63,7 +62,7 @@ export default class MessageComposer extends React.Component {
isRichTextEnabled: SettingsStore.getValue('MessageComposerInput.isRichTextEnabled'),
},
showFormatting: SettingsStore.getValue('MessageComposer.showFormatting'),
isQuoting: Boolean(RoomViewStore.getQuotingEvent()),
isQuoting: Boolean(this.props.roomViewStore.getQuotingEvent()),
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.
MatrixClientPeg.get().on("event", this.onEvent);
MatrixClientPeg.get().on("RoomState.events", this._onRoomStateEvents);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
this._waitForOwnMember();
}
@ -124,14 +123,14 @@ export default class MessageComposer extends React.Component {
}
_onRoomViewStoreUpdate() {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
const isQuoting = Boolean(this.props.roomViewStore.getQuotingEvent());
if (this.state.isQuoting === isQuoting) return;
this.setState({ isQuoting });
}
onUploadClick(ev) {
if (MatrixClientPeg.get().isGuest()) {
dis.dispatch({action: 'require_registration'});
this.props.roomViewStore.getDispatcher().dispatch({action: 'require_registration'});
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;
if (isQuoting) {
replyToWarning = <p>{
@ -229,7 +228,7 @@ export default class MessageComposer extends React.Component {
if (!call) {
return;
}
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'hangup',
// 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)
@ -238,7 +237,7 @@ export default class MessageComposer extends React.Component {
}
onCallClick(ev) {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'place_call',
type: ev.shiftKey ? "screensharing" : "video",
room_id: this.props.room.roomId,
@ -246,7 +245,7 @@ export default class MessageComposer extends React.Component {
}
onVoiceCallClick(ev) {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'place_call',
type: "voice",
room_id: this.props.room.roomId,
@ -282,7 +281,7 @@ export default class MessageComposer extends React.Component {
ev.preventDefault();
const replacementRoomId = this.state.tombstone.getContent()['replacement_room'];
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'view_room',
highlighted: true,
room_id: replacementRoomId,
@ -421,8 +420,10 @@ export default class MessageComposer extends React.Component {
controls.push(
<MessageComposerInput
roomViewStore={this.props.roomViewStore}
ref={(c) => this.messageComposerInput = c}
key="controls_input"
isGrid={this.props.isGrid}
onResize={this.props.onResize}
room={this.props.room}
placeholder={placeholderText}
@ -529,5 +530,6 @@ MessageComposer.propTypes = {
uploadAllowed: PropTypes.func.isRequired,
// string representing the current room app drawer state
showApps: PropTypes.bool
showApps: PropTypes.bool,
roomViewStore: PropTypes.object.isRequired,
};

View file

@ -41,8 +41,6 @@ import sdk from '../../../index';
import { _t, _td } from '../../../languageHandler';
import Analytics from '../../../Analytics';
import dis from '../../../dispatcher';
import * as RichText from '../../../RichText';
import * as HtmlUtils from '../../../HtmlUtils';
import Autocomplete from './Autocomplete';
@ -58,7 +56,6 @@ import {asciiRegexp, unicodeRegexp, shortnameToUnicode, emojioneList, asciiList,
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
import {makeUserPermalink} from "../../../matrix-to";
import ReplyPreview from "./ReplyPreview";
import RoomViewStore from '../../../stores/RoomViewStore';
import ReplyThread from "../elements/ReplyThread";
import {ContentHelpers} from 'matrix-js-sdk';
@ -121,7 +118,7 @@ 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({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'message_send_failed',
});
}
@ -135,6 +132,18 @@ function rangeEquals(a: Range, b: Range): boolean {
&& 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
*/
@ -150,6 +159,7 @@ export default class MessageComposerInput extends React.Component {
onFilesPasted: PropTypes.func,
onInputStateChanged: PropTypes.func,
roomViewStore: PropTypes.object.isRequired,
};
client: MatrixClient;
@ -344,12 +354,16 @@ export default class MessageComposerInput extends React.Component {
}
componentWillMount() {
this.dispatcherRef = dis.register(this.onAction);
this.historyManager = new ComposerHistoryManager(this.props.room.roomId, 'mx_slate_composer_history_');
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_');
}
}
componentWillUnmount() {
dis.unregister(this.dispatcherRef);
this.props.roomViewStore.getDispatcher().unregister(this.dispatcherRef);
}
_collectEditor = (e) => {
@ -1120,7 +1134,7 @@ export default class MessageComposerInput extends React.Component {
return true;
}
const replyingToEv = RoomViewStore.getQuotingEvent();
const replyingToEv = this.props.roomViewStore.getQuotingEvent();
const mustSendHTML = Boolean(replyingToEv);
if (this.state.isRichTextEnabled) {
@ -1208,14 +1222,14 @@ export default class MessageComposerInput extends React.Component {
// Clear reply_to_event as we put the message into the queue
// if the send fails, retry will handle resending.
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'reply_to_event',
event: null,
});
}
this.client.sendMessage(this.props.room.roomId, content).then((res) => {
dis.dispatch({
this.props.roomViewStore.getDispatcher().dispatch({
action: 'message_sent',
});
}).catch((e) => {
@ -1260,7 +1274,7 @@ export default class MessageComposerInput extends React.Component {
}
};
selectHistory = async(up) => {
selectHistory = async (up) => {
const delta = up ? -1 : 1;
// True if we are not currently selecting history, but composing a message
@ -1308,7 +1322,7 @@ export default class MessageComposerInput extends React.Component {
return true;
};
onTab = async(e) => {
onTab = async (e) => {
this.setState({
someCompletions: null,
});
@ -1330,7 +1344,7 @@ export default class MessageComposerInput extends React.Component {
up ? this.autocomplete.onUpArrow() : this.autocomplete.onDownArrow();
};
onEscape = async(e) => {
onEscape = async (e) => {
e.preventDefault();
if (this.autocomplete) {
this.autocomplete.onEscape(e);
@ -1349,7 +1363,7 @@ export default class MessageComposerInput extends React.Component {
/* 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.
*/
setDisplayedCompletion = async(displayedCompletion: ?Completion): boolean => {
setDisplayedCompletion = async (displayedCompletion: ?Completion): boolean => {
const activeEditorState = this.state.originalEditorState || this.state.editorState;
if (displayedCompletion == null) {
@ -1589,7 +1603,7 @@ export default class MessageComposerInput extends React.Component {
return (
<div className="mx_MessageComposer_input_wrapper" onClick={this.focusComposer}>
<div className="mx_MessageComposer_autocomplete_wrapper">
<ReplyPreview />
<ReplyPreview roomViewStore={this.props.roomViewStore} />
<Autocomplete
ref={(e) => this.autocomplete = e}
room={this.props.room}

View file

@ -18,7 +18,6 @@ import React from 'react';
import dis from '../../../dispatcher';
import sdk from '../../../index';
import { _t } from '../../../languageHandler';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
function cancelQuoting() {
@ -38,7 +37,7 @@ export default class ReplyPreview extends React.Component {
this._onRoomViewStoreUpdate = this._onRoomViewStoreUpdate.bind(this);
this._roomStoreToken = RoomViewStore.addListener(this._onRoomViewStoreUpdate);
this._roomStoreToken = this.props.roomViewStore.addListener(this._onRoomViewStoreUpdate);
this._onRoomViewStoreUpdate();
}
@ -50,7 +49,7 @@ export default class ReplyPreview extends React.Component {
}
_onRoomViewStoreUpdate() {
const event = RoomViewStore.getQuotingEvent();
const event = this.props.roomViewStore.getQuotingEvent();
if (this.state.event !== event) {
this.setState({ event });
}

View file

@ -24,6 +24,7 @@ import { _t } from '../../../languageHandler';
import MatrixClientPeg from '../../../MatrixClientPeg';
import Modal from "../../../Modal";
import RateLimitedFunc from '../../../ratelimitedfunc';
import dis from '../../../dispatcher';
import * as linkify from 'linkifyjs';
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() {
const currentPinEvent = this.props.room.currentState.getStateEvents("m.room.pinned_events", '');
if (!currentPinEvent) return false;
@ -409,6 +418,17 @@ module.exports = React.createClass({
</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 (
<div className={"mx_RoomHeader light-panel " + (this.props.editing ? "mx_RoomHeader_editing" : "")}>
<div className="mx_RoomHeader_wrapper">
@ -419,7 +439,8 @@ module.exports = React.createClass({
{ saveButton }
{ cancelButton }
{ rightRow }
<RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} />
{ !this.props.isGrid ? <RoomHeaderButtons collapsedRhs={this.props.collapsedRhs} /> : undefined }
{ toggleRightPanelButton }
</div>
</div>
);

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");
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 Modal from "../../../Modal";
import MatrixClientPeg from "../../../MatrixClientPeg";
import SettingsStore, {SettingLevel} from "../../../settings/SettingsStore";
export default class RoomRecoveryReminder extends React.PureComponent {
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) {
@ -82,7 +88,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
Modal.createTrackedDialog('Device Verify Dialog', '', DeviceVerifyDialog, {
userId: MatrixClientPeg.get().credentials.userId,
device: this.state.unverifiedDevice,
onFinished: this.props.onFinished,
});
return;
}
@ -91,9 +96,6 @@ export default class RoomRecoveryReminder extends React.PureComponent {
// we'll show the create key backup flow.
Modal.createTrackedDialogAsync("Key Backup", "Key Backup",
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",
import("../../../async-components/views/dialogs/keybackup/IgnoreRecoveryReminderDialog"),
{
onDontAskAgain: () => {
// Report false to the caller, who should prevent the
// reminder from appearing in the future.
this.props.onFinished(false);
onDontAskAgain: async () => {
await SettingsStore.setValue(
"showRoomRecoveryReminder",
null,
SettingLevel.ACCOUNT,
false,
);
this.props.onDontAskAgainSet();
},
onSetup: () => {
this.showSetupDialog();

View file

@ -29,7 +29,6 @@ import * as RoomNotifs from '../../../RoomNotifs';
import * as FormattingUtils from '../../../utils/FormattingUtils';
import AccessibleButton from '../elements/AccessibleButton';
import ActiveRoomObserver from '../../../ActiveRoomObserver';
import RoomViewStore from '../../../stores/RoomViewStore';
import SettingsStore from "../../../settings/SettingsStore";
module.exports = React.createClass({
@ -62,7 +61,7 @@ module.exports = React.createClass({
roomName: this.props.room.name,
notifState: RoomNotifs.getRoomNotifsState(this.props.room.roomId),
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({
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 Modal from '../../../Modal';
export default class KeyBackupPanel extends React.Component {
export default class KeyBackupPanel extends React.PureComponent {
constructor(props) {
super(props);
this._startNewBackup = this._startNewBackup.bind(this);
this._deleteBackup = this._deleteBackup.bind(this);
this._verifyDevice = this._verifyDevice.bind(this);
this._onKeyBackupSessionsRemaining =
this._onKeyBackupSessionsRemaining.bind(this);
this._onKeyBackupStatus = this._onKeyBackupStatus.bind(this);
this._restoreBackup = this._restoreBackup.bind(this);
@ -36,6 +38,7 @@ export default class KeyBackupPanel extends React.Component {
loading: true,
error: null,
backupInfo: null,
sessionsRemaining: 0,
};
}
@ -43,6 +46,10 @@ export default class KeyBackupPanel extends React.Component {
this._loadBackupStatus();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
MatrixClientPeg.get().on(
'crypto.keyBackupSessionsRemaining',
this._onKeyBackupSessionsRemaining,
);
}
componentWillUnmount() {
@ -50,9 +57,19 @@ export default class KeyBackupPanel extends React.Component {
if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener('crypto.keyBackupStatus', this._onKeyBackupStatus);
MatrixClientPeg.get().removeListener(
'crypto.keyBackupSessionsRemaining',
this._onKeyBackupSessionsRemaining,
);
}
}
_onKeyBackupSessionsRemaining(sessionsRemaining) {
this.setState({
sessionsRemaining,
});
}
_onKeyBackupStatus() {
this._loadBackupStatus();
}
@ -144,57 +161,70 @@ export default class KeyBackupPanel extends React.Component {
} else if (this.state.backupInfo) {
let clientBackupStatus;
if (MatrixClientPeg.get().getKeyBackupEnabled()) {
clientBackupStatus = _t("This device is uploading keys to this backup");
clientBackupStatus = _t("This device is using key backup");
} else {
// XXX: display why and how to fix it
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>},
);
}
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) => {
const deviceName = sig.device.getDisplayName() || sig.device.deviceId;
const sigStatusSubstitutions = {
validity: sub =>
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
{sub}
</span>,
verify: sub =>
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
{sub}
</span>,
device: sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>,
};
const validity = sub =>
<span className={sig.valid ? 'mx_KeyBackupPanel_sigValid' : 'mx_KeyBackupPanel_sigInvalid'}>
{sub}
</span>;
const verify = sub =>
<span className={sig.device.isVerified() ? 'mx_KeyBackupPanel_deviceVerified' : 'mx_KeyBackupPanel_deviceNotVerified'}>
{sub}
</span>;
const device = sub => <span className="mx_KeyBackupPanel_deviceName">{deviceName}</span>;
let sigStatus;
if (sig.device.getFingerprint() === MatrixClientPeg.get().getDeviceEd25519Key()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from this device",
{}, sigStatusSubstitutions,
{}, { validity },
);
} else if (sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>verified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
} else if (sig.valid && !sig.device.isVerified()) {
sigStatus = _t(
"Backup has a <validity>valid</validity> signature from " +
"<verify>unverified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
} else if (!sig.valid && sig.device.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<verify>verified</verify> device <device></device>",
{}, sigStatusSubstitutions,
{}, { validity, verify, device },
);
} else if (!sig.valid && !sig.device.isVerified()) {
sigStatus = _t(
"Backup has an <validity>invalid</validity> signature from " +
"<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("Algorithm: ")}{this.state.backupInfo.algorithm}<br />
{clientBackupStatus}<br />
{uploadStatus}
<div>{backupSigStatuses}</div><br />
<br />
<AccessibleButton className="mx_UserSettings_button"

View file

@ -17,42 +17,10 @@ limitations under the License.
'use strict';
const flux = require("flux");
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);
}
}
}
import MatrixDispatcher from "./matrix-dispatcher";
if (global.mxDispatcher === undefined) {
global.mxDispatcher = new MatrixDispatcher();
}
module.exports = global.mxDispatcher;

View file

@ -352,8 +352,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 backup": "Delete backup",
"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 <b>not</b> uploading keys to this backup": "This device is <b>not</b> uploading keys to this backup",
"This device is using key backup": "This device is using key 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 <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>",
@ -967,7 +969,12 @@
"Clear cache and resync": "Clear cache and resync",
"Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!": "Riot now uses 3-5x less memory, by only loading information about other users when needed. Please wait whilst we resynchronise with the server!",
"Updating Riot": "Updating Riot",
"When you log out, you'll lose your secure message history. To prevent this, set up a recovery method.": "When you log out, you'll lose your secure message history. To prevent this, set up a recovery method.",
"Alternatively, advanced users can also manually export encryption keys in <a>Settings</a> before logging out.": "Alternatively, advanced users can also manually export encryption keys in <a>Settings</a> before logging out.",
"For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.": "For security, logging out will delete any end-to-end encryption keys from this browser. If you want to be able to decrypt your conversation history from future Riot sessions, please export your room keys for safe-keeping.",
"Set a Recovery Method": "Set a Recovery Method",
"I understand, log out without": "I understand, log out without",
"When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery key.": "When signing in again, you can access encrypted chat history by restoring your key backup. You'll need your recovery key.",
"Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.": "Thanks for testing the Riot Redesign. If you run into any bugs or visual issues, please let us know on GitHub.",
"To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.": "To help avoid duplicate issues, please <existingIssuesLink>view existing issues</existingIssuesLink> first (and add a +1) or <newIssueLink>create a new issue</newIssueLink> if you can't find it.",
"Report bugs & give feedback": "Report bugs & give feedback",
@ -1376,21 +1383,20 @@
"Your Recovery Key": "Your Recovery Key",
"Copy to clipboard": "Copy to clipboard",
"Download": "Download",
"I've made a copy": "I've made a copy",
"Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:": "Your Recovery Key has been <b>copied to your clipboard</b>, paste it to:",
"Your Recovery Key is in your <b>Downloads</b> folder.": "Your Recovery Key is in your <b>Downloads</b> folder.",
"<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>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 to your Homeserver.": "Your encryption keys are now being backed up to your Homeserver.",
"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.",
"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",
"Create a Recovery Passphrase": "Create a Recovery Passphrase",
"Confirm Recovery Passphrase": "Confirm Recovery Passphrase",
"Recovery Key": "Recovery Key",
"Keep it safe": "Keep it safe",
"Backing up...": "Backing up...",
"Starting backup...": "Starting backup...",
"Backup Started": "Backup Started",
"Create Key Backup": "Create Key Backup",
"Unable to create key backup": "Unable to create key backup",
"Retry": "Retry",
@ -1405,5 +1411,8 @@
"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 add tag %(tagName)s to room": "Failed to add tag %(tagName)s to room",
"Render simple counters in room header": "Render simple counters in room header"
"Render simple counters in room header": "Render simple counters in room header",
"View as Grid": "View as Grid",
"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",
"No room in this tile yet.": "No room in this tile yet."
}

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

@ -108,6 +108,12 @@ export const SETTINGS = {
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": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
displayName: _td('Disable Emoji suggestions while typing'),

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

@ -277,7 +277,7 @@ class RoomListStore extends Store {
const roomCache = this._state.roomCache;
if (!roomCache[roomId]) roomCache[roomId] = {};
if (value) roomCache[roomId][type] = value;
if (typeof value !== "undefined") roomCache[roomId][type] = value;
else delete roomCache[roomId][type];
this._setState({roomCache});

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
limitations under the License.
*/
import dis from '../dispatcher';
import {Store} from 'flux/utils';
import MatrixClientPeg from '../MatrixClientPeg';
import sdk from '../index';
@ -53,12 +52,12 @@ const INITIAL_STATE = {
* with a subset of the js-sdk.
* ```
*/
class RoomViewStore extends Store {
constructor() {
super(dis);
export class RoomViewStore extends Store {
constructor(dispatcher) {
super(dispatcher);
// Initialise state
this._state = INITIAL_STATE;
this._state = Object.assign({}, INITIAL_STATE);
}
_setState(newState) {
@ -85,6 +84,8 @@ class RoomViewStore extends Store {
});
break;
case 'view_room_error':
// should not go over dispatcher anymore
// but be internal to RoomViewStore
this._viewRoomError(payload);
break;
case 'will_join':
@ -150,22 +151,11 @@ class RoomViewStore extends Store {
// pull the user out of Room Settings
isEditingSettings: false,
};
if (this._state.forwardingEvent) {
dis.dispatch({
action: 'send_event',
room_id: newState.roomId,
event: this._state.forwardingEvent,
});
}
this._setState(newState);
if (payload.auto_join) {
this._joinRoom(payload);
}
} else if (payload.room_alias) {
// Resolve the alias and then do a second dispatch with the room ID acquired
this._setState({
roomId: null,
initialEventId: null,
@ -175,25 +165,6 @@ class RoomViewStore extends Store {
roomLoading: true,
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
// the user joined to the room.
}, (err) => {
dis.dispatch({
this.getDispatcher().dispatch({
action: 'join_room_error',
err: err,
});
@ -335,8 +306,7 @@ class RoomViewStore extends Store {
}
}
let singletonRoomViewStore = null;
if (!singletonRoomViewStore) {
singletonRoomViewStore = new RoomViewStore();
}
module.exports = singletonRoomViewStore;
const MatrixDispatcher = require("../matrix-dispatcher");
const backwardsCompatInstance = new RoomViewStore(new MatrixDispatcher());
export default backwardsCompatInstance;

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()`.
*/
export default class Timer {
constructor(timeout) {
this._timeout = timeout;
this._onTimeout = this._onTimeout.bind(this);
@ -70,6 +69,7 @@ export default class Timer {
/**
* if not started before, starts the timer.
* @returns {Timer} the same timer
*/
start() {
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.
* @returns {Timer} the same timer
*/
restart() {
if (this.isRunning()) {
@ -98,6 +99,7 @@ export default class Timer {
/**
* if the timer is running, abort it,
* and reject the promise for this timer.
* @returns {Timer} the same timer
*/
abort() {
if (this.isRunning()) {