{ this.props.children }
diff --git a/src/components/structures/GroupView.js b/src/components/structures/GroupView.js
index 530a200a18..89fce9c718 100644
--- a/src/components/structures/GroupView.js
+++ b/src/components/structures/GroupView.js
@@ -1157,7 +1157,6 @@ export default React.createClass({
render: function() {
const GroupAvatar = sdk.getComponent("avatars.GroupAvatar");
const Spinner = sdk.getComponent("elements.Spinner");
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
const GeminiScrollbarWrapper = sdk.getComponent("elements.GeminiScrollbarWrapper");
if (this.state.summaryLoading && this.state.error === null || this.state.saving) {
@@ -1248,13 +1247,17 @@ export default React.createClass({
if (this.state.editing) {
rightButtons.push(
{ _t('Save') }
,
);
rightButtons.push(
-
+
,
@@ -1262,16 +1265,20 @@ export default React.createClass({
} else {
if (summary.user && summary.user.membership === 'join') {
rightButtons.push(
-
-
,
);
}
rightButtons.push(
-
-
+
,
);
}
diff --git a/src/components/structures/IndicatorScrollbar.js b/src/components/structures/IndicatorScrollbar.js
index c3e54ee900..e1516d1f64 100644
--- a/src/components/structures/IndicatorScrollbar.js
+++ b/src/components/structures/IndicatorScrollbar.js
@@ -59,6 +59,10 @@ export default class IndicatorScrollbar extends React.Component {
}
}
+ getScrollTop() {
+ return this._autoHideScrollbar.getScrollTop();
+ }
+
componentWillUnmount() {
if (this._scrollElement) {
this._scrollElement.removeEventListener("scroll", this.checkOverflow);
diff --git a/src/components/structures/MatrixChat.js b/src/components/structures/MatrixChat.js
index 4bb4e34033..e534ced577 100644
--- a/src/components/structures/MatrixChat.js
+++ b/src/components/structures/MatrixChat.js
@@ -654,11 +654,9 @@ export default React.createClass({
});
break;
}
- // case 'set_theme':
- // disable changing the theme for now
- // as other themes are not compatible with dharma
- // this._onSetTheme(payload.value);
- // break;
+ case 'set_theme':
+ this._onSetTheme(payload.value);
+ break;
case 'on_logging_in':
// We are now logging in, so set the state to reflect that
// NB. This does not touch 'ready' since if our dispatches
diff --git a/src/components/structures/RoomSubList.js b/src/components/structures/RoomSubList.js
index ca2be85b35..f7f74da728 100644
--- a/src/components/structures/RoomSubList.js
+++ b/src/components/structures/RoomSubList.js
@@ -27,7 +27,8 @@ import IndicatorScrollbar from './IndicatorScrollbar';
import { KeyCode } from '../../Keyboard';
import { Group } from 'matrix-js-sdk';
import PropTypes from 'prop-types';
-
+import RoomTile from "../views/rooms/RoomTile";
+import LazyRenderList from "../views/elements/LazyRenderList";
// turn this on for drop & drag console debugging galore
const debug = false;
@@ -60,6 +61,9 @@ const RoomSubList = React.createClass({
getInitialState: function() {
return {
hidden: this.props.startAsHidden || false,
+ // some values to get LazyRenderList starting
+ scrollerHeight: 800,
+ scrollTop: 0,
};
},
@@ -134,24 +138,21 @@ const RoomSubList = React.createClass({
this.setState(this.state);
},
- makeRoomTiles: function() {
- const RoomTile = sdk.getComponent("rooms.RoomTile");
- return this.props.list.map((room, index) => {
- return 0 || this.props.isInvite}
- notificationCount={room.getUnreadNotificationCount()}
- isInvite={this.props.isInvite}
- refreshSubList={this._updateSubListCount}
- incomingCall={null}
- onClick={this.onRoomTileClick}
- />;
- });
+ makeRoomTile: function(room) {
+ return 0 || this.props.isInvite}
+ notificationCount={room.getUnreadNotificationCount()}
+ isInvite={this.props.isInvite}
+ refreshSubList={this._updateSubListCount}
+ incomingCall={null}
+ onClick={this.onRoomTileClick}
+ />;
},
_onNotifBadgeClick: function(e) {
@@ -270,6 +271,29 @@ const RoomSubList = React.createClass({
if (this.refs.subList) {
this.refs.subList.style.height = `${height}px`;
}
+ this._updateLazyRenderHeight(height);
+ },
+
+ _updateLazyRenderHeight: function(height) {
+ this.setState({scrollerHeight: height});
+ },
+
+ _onScroll: function() {
+ this.setState({scrollTop: this.refs.scroller.getScrollTop()});
+ },
+
+ _getRenderItems: function() {
+ // try our best to not create a new array
+ // because LazyRenderList rerender when the items prop
+ // is not the same object as the previous value
+ const {list, extraTiles} = this.props;
+ if (!extraTiles || !extraTiles.length) {
+ return list;
+ }
+ if (!list || list.length) {
+ return extraTiles;
+ }
+ return list.concat(extraTiles);
},
render: function() {
@@ -287,12 +311,15 @@ const RoomSubList = React.createClass({
{this._getHeaderJsx(isCollapsed)}
;
} else {
- const tiles = this.makeRoomTiles();
- tiles.push(...this.props.extraTiles);
return
{this._getHeaderJsx(isCollapsed)}
-
- { tiles }
+
+
;
}
diff --git a/src/components/structures/SearchBox.js b/src/components/structures/SearchBox.js
index 2f777c1163..10628ccd13 100644
--- a/src/components/structures/SearchBox.js
+++ b/src/components/structures/SearchBox.js
@@ -21,7 +21,7 @@ import { _t } from '../../languageHandler';
import { KeyCode } from '../../Keyboard';
import sdk from '../../index';
import dis from '../../dispatcher';
-import { debounce } from 'lodash';
+import { throttle } from 'lodash';
import AccessibleButton from '../../components/views/elements/AccessibleButton';
module.exports = React.createClass({
@@ -67,9 +67,9 @@ module.exports = React.createClass({
this.onSearch();
},
- onSearch: debounce(function() {
+ onSearch: throttle(function() {
this.props.onSearch(this.refs.search.value);
- }, 200, {trailing: true}),
+ }, 200, {trailing: true, leading: true}),
_onKeyDown: function(ev) {
switch (ev.keyCode) {
diff --git a/src/components/structures/auth/Registration.js b/src/components/structures/auth/Registration.js
index 69abd8b88b..6b578f0f68 100644
--- a/src/components/structures/auth/Registration.js
+++ b/src/components/structures/auth/Registration.js
@@ -64,6 +64,17 @@ module.exports = React.createClass({
getInitialState: function() {
const customURLsAllowed = !SdkConfig.get()['disable_custom_urls'];
+ let initialPhase = PHASE_SERVER_DETAILS;
+ if (
+ // if we have these two, skip to the good bit
+ // (they could come in from the URL params in a
+ // registration email link)
+ (this.props.clientSecret && this.props.sessionId) ||
+ // or if custom URLs aren't allowed, skip them
+ !customURLsAllowed
+ ) {
+ initialPhase = PHASE_REGISTRATION;
+ }
return {
busy: false,
@@ -87,7 +98,7 @@ module.exports = React.createClass({
hsUrl: this.props.customHsUrl,
isUrl: this.props.customIsUrl,
// Phase of the overall registration dialog.
- phase: customURLsAllowed ? PHASE_SERVER_DETAILS : PHASE_REGISTRATION,
+ phase: initialPhase,
flows: null,
};
},
@@ -111,7 +122,7 @@ module.exports = React.createClass({
});
},
- onServerTypeChange(type) {
+ onServerTypeChange(type, initial) {
this.setState({
serverType: type,
});
@@ -137,9 +148,15 @@ module.exports = React.createClass({
hsUrl: this.props.defaultHsUrl,
isUrl: this.props.defaultIsUrl,
});
- this.setState({
- phase: PHASE_SERVER_DETAILS,
- });
+ // if this is the initial value from the control and we're
+ // already in the registration phase, don't go back to the
+ // server details phase (but do if it's actually a change resulting
+ // from user interaction).
+ if (!initial || !this.state.phase === PHASE_REGISTRATION) {
+ this.setState({
+ phase: PHASE_SERVER_DETAILS,
+ });
+ }
break;
}
},
@@ -372,9 +389,12 @@ module.exports = React.createClass({
// If we're on a different phase, we only show the server type selector,
// which is always shown if we allow custom URLs at all.
if (PHASES_ENABLED && this.state.phase !== PHASE_SERVER_DETAILS) {
+ // if we've been given a custom HS URL we should actually pass that, so
+ // that the appropriate section is selected at the start to match the
+ // homeserver URL we're using
return
;
diff --git a/src/components/views/auth/ServerTypeSelector.js b/src/components/views/auth/ServerTypeSelector.js
index de76e6acf9..7a28eec0ed 100644
--- a/src/components/views/auth/ServerTypeSelector.js
+++ b/src/components/views/auth/ServerTypeSelector.js
@@ -90,7 +90,11 @@ export default class ServerTypeSelector extends React.PureComponent {
selected: type,
};
if (onChange) {
- onChange(type);
+ // FIXME: Supply a second 'initial' param here to flag that this is
+ // initialising the value rather than from user interaction
+ // (which sometimes we'll want to ignore). Must be a better way
+ // to do this.
+ onChange(type, true);
}
}
diff --git a/src/components/views/context_menus/TopLeftMenu.js b/src/components/views/context_menus/TopLeftMenu.js
index 1d58db3c49..8583f631f2 100644
--- a/src/components/views/context_menus/TopLeftMenu.js
+++ b/src/components/views/context_menus/TopLeftMenu.js
@@ -20,11 +20,15 @@ import { _t } from '../../../languageHandler';
import LogoutDialog from "../dialogs/LogoutDialog";
import Modal from "../../../Modal";
import SdkConfig from '../../../SdkConfig';
+import MatrixClientPeg from '../../../MatrixClientPeg';
export class TopLeftMenu extends React.Component {
constructor() {
super();
+ this.viewHomePage = this.viewHomePage.bind(this);
+ this.viewWelcomePage = this.viewWelcomePage.bind(this);
this.openSettings = this.openSettings.bind(this);
+ this.signIn = this.signIn.bind(this);
this.signOut = this.signOut.bind(this);
}
@@ -41,6 +45,8 @@ export class TopLeftMenu extends React.Component {
}
render() {
+ const isGuest = MatrixClientPeg.get().isGuest();
+
let homePageSection = null;
if (this.hasHomePage()) {
homePageSection =
@@ -48,14 +54,26 @@ export class TopLeftMenu extends React.Component {
;
}
+ let signInOutSection;
+ if (isGuest) {
+ signInOutSection =
;
+ } else {
+ signInOutSection =
;
+ }
+
return
{homePageSection}
- - {_t("Settings")}
+ - {_t("Welcome")}
- - {_t("Sign out")}
+ - {_t("Settings")}
+ {signInOutSection}
;
}
@@ -64,11 +82,21 @@ export class TopLeftMenu extends React.Component {
this.closeMenu();
}
+ viewWelcomePage() {
+ dis.dispatch({action: 'view_welcome_page'});
+ this.closeMenu();
+ }
+
openSettings() {
dis.dispatch({action: 'view_user_settings'});
this.closeMenu();
}
+ signIn() {
+ dis.dispatch({action: 'start_login'});
+ this.closeMenu();
+ }
+
signOut() {
Modal.createTrackedDialog('Logout E2E Export', '', LogoutDialog);
this.closeMenu();
diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js
new file mode 100644
index 0000000000..1c1cc127d6
--- /dev/null
+++ b/src/components/views/elements/LazyRenderList.js
@@ -0,0 +1,97 @@
+/*
+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";
+
+const OVERFLOW_ITEMS = 20;
+const OVERFLOW_MARGIN = 5;
+
+class ItemRange {
+ constructor(topCount, renderCount, bottomCount) {
+ this.topCount = topCount;
+ this.renderCount = renderCount;
+ this.bottomCount = bottomCount;
+ }
+
+ contains(range) {
+ return range.topCount >= this.topCount &&
+ (range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
+ }
+
+ expand(amount) {
+ const topGrow = Math.min(amount, this.topCount);
+ const bottomGrow = Math.min(amount, this.bottomCount);
+ return new ItemRange(
+ this.topCount - topGrow,
+ this.renderCount + topGrow + bottomGrow,
+ this.bottomCount - bottomGrow,
+ );
+ }
+}
+
+export default class LazyRenderList extends React.Component {
+ constructor(props) {
+ super(props);
+ const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS);
+ this.state = {renderRange};
+ }
+
+ static getVisibleRangeFromProps(props) {
+ const {items, itemHeight, scrollTop, height} = props;
+ const length = items ? items.length : 0;
+ const topCount = Math.max(0, Math.floor(scrollTop / itemHeight));
+ const itemsAfterTop = length - topCount;
+ const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop);
+ const bottomCount = itemsAfterTop - renderCount;
+ return new ItemRange(topCount, renderCount, bottomCount);
+ }
+
+ componentWillReceiveProps(props) {
+ const state = this.state;
+ const range = LazyRenderList.getVisibleRangeFromProps(props);
+ const intersectRange = range.expand(OVERFLOW_MARGIN);
+
+ const prevSize = this.props.items ? this.props.items.length : 0;
+ const listHasChangedSize = props.items.length !== prevSize;
+ // only update renderRange if the list has shrunk/grown and we need to adjust padding or
+ // if the new range isn't contained by the old anymore
+ if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) {
+ this.setState({renderRange: range.expand(OVERFLOW_ITEMS)});
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ const itemsChanged = nextProps.items !== this.props.items;
+ const rangeChanged = nextState.renderRange !== this.state.renderRange;
+ return itemsChanged || rangeChanged;
+ }
+
+ render() {
+ const {itemHeight, items, renderItem} = this.props;
+
+ const {renderRange} = this.state;
+ const paddingTop = renderRange.topCount * itemHeight;
+ const paddingBottom = renderRange.bottomCount * itemHeight;
+ const renderedItems = (items || []).slice(
+ renderRange.topCount,
+ renderRange.topCount + renderRange.renderCount,
+ );
+
+ return (
+ { renderedItems.map(renderItem) }
+
);
+ }
+}
diff --git a/src/components/views/elements/ManageIntegsButton.js b/src/components/views/elements/ManageIntegsButton.js
index e5cc21d85e..3240050b6a 100644
--- a/src/components/views/elements/ManageIntegsButton.js
+++ b/src/components/views/elements/ManageIntegsButton.js
@@ -24,7 +24,6 @@ import ScalarMessaging from '../../../ScalarMessaging';
import Modal from "../../../Modal";
import { _t } from '../../../languageHandler';
import AccessibleButton from './AccessibleButton';
-import TintableSvg from './TintableSvg';
export default class ManageIntegsButton extends React.Component {
constructor(props) {
@@ -76,6 +75,7 @@ export default class ManageIntegsButton extends React.Component {
if (this.scalarClient !== null) {
const integrationsButtonClasses = classNames({
mx_RoomHeader_button: true,
+ mx_RoomHeader_manageIntegsButton: true,
mx_ManageIntegsButton_error: !!this.state.scalarError,
});
@@ -94,8 +94,10 @@ export default class ManageIntegsButton extends React.Component {
}
integrationsButton = (
-
-
+
{ integrationsWarningTriangle }
{ integrationsErrorPopup }
diff --git a/src/components/views/right_panel/GroupHeaderButtons.js b/src/components/views/right_panel/GroupHeaderButtons.js
index 6867b0bb9d..13379d49e3 100644
--- a/src/components/views/right_panel/GroupHeaderButtons.js
+++ b/src/components/views/right_panel/GroupHeaderButtons.js
@@ -65,12 +65,14 @@ export default class GroupHeaderButtons extends HeaderButtons {
];
return [
- ,
-
-
- ;
+ onClick={this.onClick}>
+ ;
}
}
@@ -62,14 +61,14 @@ HeaderButton.propTypes = {
isHighlighted: PropTypes.bool.isRequired,
// The phase to swap to when the button is clicked
clickPhase: PropTypes.string.isRequired,
- // The source file of the icon to display
- iconSrc: PropTypes.string.isRequired,
// The badge to display above the icon
badge: PropTypes.node,
// The parameters to track the click event
analytics: PropTypes.arrayOf(PropTypes.string).isRequired,
+ // Button name
+ name: PropTypes.string.isRequired,
// Button title
title: PropTypes.string.isRequired,
};
diff --git a/src/components/views/right_panel/HeaderButtons.js b/src/components/views/right_panel/HeaderButtons.js
index 3f5f58121d..b7155b922f 100644
--- a/src/components/views/right_panel/HeaderButtons.js
+++ b/src/components/views/right_panel/HeaderButtons.js
@@ -88,7 +88,7 @@ export default class HeaderButtons extends React.Component {
render() {
// inline style as this will be swapped around in future commits
- return
+ return
{ this.renderButtons() }
;
}
diff --git a/src/components/views/right_panel/RoomHeaderButtons.js b/src/components/views/right_panel/RoomHeaderButtons.js
index 1985909f4a..8d10094637 100644
--- a/src/components/views/right_panel/RoomHeaderButtons.js
+++ b/src/components/views/right_panel/RoomHeaderButtons.js
@@ -52,17 +52,20 @@ export default class RoomHeaderButtons extends HeaderButtons {
];
return [
-
,
-
,
-
-
+
;
} else {
callButton =
-
-
+
;
videoCallButton =
-
-
+
;
}
@@ -385,9 +393,11 @@ export default class MessageComposer extends React.Component {
// check separately for whether we can call, but this is slightly
// complex because of conference calls.
const uploadButton = (
-
-
+
-
+
;
}
@@ -245,7 +246,6 @@ module.exports = React.createClass({
{ pinsIndicator }
-
;
}
@@ -260,24 +260,30 @@ module.exports = React.createClass({
let forgetButton;
if (this.props.onForgetClick) {
forgetButton =
-
-
+
;
}
let searchButton;
if (this.props.onSearchClick && this.props.inRoom) {
searchButton =
-
-
+
;
}
let shareRoomButton;
if (this.props.inRoom) {
shareRoomButton =
-
-
+
;
}
diff --git a/src/components/views/rooms/Stickerpicker.js b/src/components/views/rooms/Stickerpicker.js
index 188b42622a..c9f3195b08 100644
--- a/src/components/views/rooms/Stickerpicker.js
+++ b/src/components/views/rooms/Stickerpicker.js
@@ -322,7 +322,6 @@ export default class Stickerpicker extends React.Component {
}
render() {
- const TintableSvg = sdk.getComponent("elements.TintableSvg");
const ContextualMenu = sdk.getComponent('structures.ContextualMenu');
const GenericElementContextMenu = sdk.getComponent('context_menus.GenericElementContextMenu');
let stickersButton;
@@ -348,11 +347,11 @@ export default class Stickerpicker extends React.Component {
-
+ title={_t("Hide Stickers")}
+ >
;
} else {
// Show show-stickers button
@@ -360,10 +359,10 @@ export default class Stickerpicker extends React.Component {
-
+ title={_t("Show Stickers")}
+ >
;
}
return
diff --git a/src/components/views/settings/KeyBackupPanel.js b/src/components/views/settings/KeyBackupPanel.js
index 3976196b57..cc1b3fd017 100644
--- a/src/components/views/settings/KeyBackupPanel.js
+++ b/src/components/views/settings/KeyBackupPanel.js
@@ -42,7 +42,7 @@ export default class KeyBackupPanel extends React.PureComponent {
}
componentWillMount() {
- this._loadBackupStatus();
+ this._checkKeyBackupStatus();
MatrixClientPeg.get().on('crypto.keyBackupStatus', this._onKeyBackupStatus);
MatrixClientPeg.get().on(
@@ -70,9 +70,32 @@ export default class KeyBackupPanel extends React.PureComponent {
}
_onKeyBackupStatus() {
+ // This just loads the current backup status rather than forcing
+ // a re-check otherwise we risk causing infinite loops
this._loadBackupStatus();
}
+ async _checkKeyBackupStatus() {
+ try {
+ const {backupInfo, trustInfo} = await MatrixClientPeg.get().checkKeyBackup();
+ this.setState({
+ backupInfo,
+ backupSigStatus: trustInfo,
+ error: null,
+ loading: false,
+ });
+ } catch (e) {
+ console.log("Unable to fetch check backup status", e);
+ if (this._unmounted) return;
+ this.setState({
+ error: e,
+ backupInfo: null,
+ backupSigStatus: null,
+ loading: false,
+ });
+ }
+ }
+
async _loadBackupStatus() {
this.setState({loading: true});
try {
@@ -80,6 +103,7 @@ export default class KeyBackupPanel extends React.PureComponent {
const backupSigStatus = await MatrixClientPeg.get().isKeyBackupTrusted(backupInfo);
if (this._unmounted) return;
this.setState({
+ error: null,
backupInfo,
backupSigStatus,
loading: false,
@@ -89,9 +113,10 @@ export default class KeyBackupPanel extends React.PureComponent {
if (this._unmounted) return;
this.setState({
error: e,
+ backupInfo: null,
+ backupSigStatus: null,
loading: false,
});
- return;
}
}
diff --git a/src/components/views/settings/tabs/GeneralUserSettingsTab.js b/src/components/views/settings/tabs/GeneralUserSettingsTab.js
index bfcc7b945c..2364475239 100644
--- a/src/components/views/settings/tabs/GeneralUserSettingsTab.js
+++ b/src/components/views/settings/tabs/GeneralUserSettingsTab.js
@@ -54,6 +54,7 @@ export default class GeneralUserSettingsTab extends React.Component {
if (this.state.theme === newTheme) return;
SettingsStore.setValue("theme", null, SettingLevel.ACCOUNT, newTheme);
+ this.setState({theme: newTheme});
dis.dispatch({action: 'set_theme', value: newTheme});
};
@@ -138,17 +139,14 @@ export default class GeneralUserSettingsTab extends React.Component {
}
_renderThemeSection() {
- // TODO: Re-enable theme selection once the themes actually work
const SettingsFlag = sdk.getComponent("views.elements.SettingsFlag");
return (
{_t("Theme")}
-
-
+
-
-
@@ -164,7 +162,7 @@ export default class GeneralUserSettingsTab extends React.Component {
{_t("Deactivating your account is a permanent action - be careful!")}
- {_t("Close Account")}
+ {_t("Deactivate Account")}
);
diff --git a/src/components/views/settings/tabs/VoiceSettingsTab.js b/src/components/views/settings/tabs/VoiceSettingsTab.js
index 65f38c7841..aefb114dd3 100644
--- a/src/components/views/settings/tabs/VoiceSettingsTab.js
+++ b/src/components/views/settings/tabs/VoiceSettingsTab.js
@@ -174,7 +174,7 @@ export default class VoiceSettingsTab extends React.Component {
{microphoneDropdown}
{webcamDropdown}
-
+
);
diff --git a/src/i18n/strings/bg.json b/src/i18n/strings/bg.json
index 31b8d5b209..e92648a0d2 100644
--- a/src/i18n/strings/bg.json
+++ b/src/i18n/strings/bg.json
@@ -1515,7 +1515,7 @@
"2018 theme": "Тема 2018",
"Account management": "Управление на акаунта",
"Deactivating your account is a permanent action - be careful!": "Деактивирането на акаунта е необратимо действие - внимавайте!",
- "Close Account": "Затвори акаунта",
+ "Deactivate Account": "Затвори акаунта",
"For help with using Riot, click